diff --git a/pom.xml b/pom.xml
index 44b1e51de..aae488431 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,6 +104,13 @@
provided
+
+ com.mysema.querydsl
+ querydsl-collections
+ ${querydsl}
+ true
+
+
javax.ejb
@@ -148,6 +155,7 @@
${springhateoas}true
+
org.springframeworkspring-webmvc
@@ -175,6 +183,21 @@
1.8.6test
+
+
+ net.sf.ehcache
+ ehcache
+ 2.8.3
+ true
+
+
+
+ com.hazelcast
+ hazelcast
+ 3.3
+ true
+
+
diff --git a/src/main/asciidoc/key-value-repositories.adoc b/src/main/asciidoc/key-value-repositories.adoc
new file mode 100644
index 000000000..e10fa862f
--- /dev/null
+++ b/src/main/asciidoc/key-value-repositories.adoc
@@ -0,0 +1,147 @@
+:spring-framework-docs: http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html
+
+[[key-value]]
+= Key Value Repositories
+
+This chapter explains concepts and usage patterns when working with the key value abstraction and the `java.util.Map` based implementation provided by Spring Data Commons.
+
+[[key-value.core-concepts]]
+== Core Concepts
+
+The Key/Value abstraction within Spring Data Commons requires an `Adapter` shielding the native store implementation freeing up `KeyValueTemplate` to work on top of any key/value pair like structure. Keys are distributed across <>. Unless otherwise specified the class name is used as the default keyspace for an entity.
+
+[source, java]
+----
+interface KeyValueOperations {
+
+ T insert(T objectToInsert); <1>
+
+ void update(Object objectToUpdate); <2>
+
+ void delete(Class> type); <3>
+
+ T findById(Serializable id, Class type); <4>
+
+ List findAllOf(Class type); <5>
+
+ List find(KeyValueQuery> query, Class type); <6>
+
+ //... more functionality omitted.
+
+}
+----
+<1> Inserts the given entity and assigns id if required.
+<2> Updates the given entity.
+<3> Removes all entities of matching type.
+<4> Returns the entity of given type with matching id.
+<5> Returns all entities of matching type.
+<6> Returns a List of all entities of given type matching the criteria of the query.
+
+[[key-value.template-configuration]]
+== Configuring The KeyValueTemplate
+
+In its very basic shape the `KeyValueTemplate` uses a `MapAdaper` wrapping a `ConcurrentHashMap` using link:{spring-framework-docs}/expressions.html[Spring Expression Language] to perform queries and sorting.
+
+NOTE: The used `KeyValueAdapter` does the heavy lifting when it comes to storing and retrieving data. The data structure used will influence performance and/or multi threading behavior.
+
+One may choose to use a different type or preinitialize the adapter with some values, and can do so using `MapKeyValueAdapterFactory`.
+
+[source, java]
+----
+@Bean
+public KeyValueOperations keyValueTemplate() {
+ return new KeyValueTemplate(keyValueAdapter());
+}
+
+@Bean
+public KeyValueAdapter keyValueAdapter() {
+
+ MapKeyValueAdapterFactory factory = new MapKeyValueAdapterFactory();
+ factory.setMapType(ConcurrentSkipListMap.class);
+ factory.setInitialValuesForKeyspace("lennister", singletonMap("1", "tyrion"));
+ factory.setInitialValuesForKeyspace("stark", singletonMap("1", "sansa"));
+
+ return factory.getAdapter();
+}
+----
+
+[[key-value.keyspaces]]
+== Keyspaces
+
+Keyspaces define in which part of the data structure the entity should be kept. So this is a rather similar concept as collections in MongoDB and Elasticsearch, Cores in Solr, Tables in JPA.
+By default the keyspace of an entity is extracted form its type, but one can also choose to store entities of different types within one keyspace. In that case any find operation will type check results.
+
+[source, java]
+----
+@KeySpace("persons")
+class Person {
+
+ @Id String id;
+ String firstname;
+ String lastname;
+}
+
+class User extends Person {
+ String username;
+}
+
+template.findAllOf(Person.class); <1>
+template.findAllOf(User.class); <2>
+----
+<1> Returns all entities for keyspace "persons".
+<2> Returns only elements of type `User` stored in keyspace "persons".
+
+[[key-value.template-query]]
+== Querying
+
+Query execution is managed by the `QueryEngine`. As mentioned before it is possible to instruct the `KeyValueAdapter` to use an implementation specific `QueryEngine` that allows access to native functionality.
+When used without further customization queries are be executed using a `SpELQueryEngine`.
+
+NOTE: For performance reasons, we highly recommend to have at least Spring 4.1.2 or better to make use of link:{spring-framework-docs}/expressions.html#expressions-spel-compilation[compiled SpEL Expressions].
+
+[source, java]
+----
+KeyValueQuery query = new KeyValueQuery("lastname == 'targaryen'");
+List targaryens = template.find(query, Person.class);
+----
+
+WARNING: Please note that you need to have getters/setters present to query properties using SpEL.
+
+[[key-value.template-sort]]
+== Sorting
+
+Depending on the store implementation provided by the adapter entities might already be stored in some sorted way but do not necessarily have to be. Again the underlying `QueryEngine` is capable of performing sort operations.
+When used without further customization sorting is done using a `SpelPropertyComperator` extracted from the `Sort` clause provided
+
+[source, java]
+----
+KeyValueQuery query = new KeyValueQuery("lastname == 'baratheon'");
+query.setSort(new Sort(DESC, "age"));
+List targaryens = template.find(query, Person.class);
+----
+
+WARNING: Please note that you need to have getters/setters present to sort using SpEL.
+
+[[key-value.repositories]]
+== Key Value Repositories
+
+KeyValue repositories reside on top of the `KeyValaueTemplate`. Using the default `SpelQueryCreator` allows deriving query and sort expressions from the given methodname.
+
+[source, java]
+----
+@Configuration
+@EnableKeyValueRepositories
+class KeyValueConfig {
+
+ @Bean
+ public KeyValueOperations keyValueTemplate() {
+ return new KeyValueTemplate(new MapKeyValueAdapter());
+ }
+
+}
+
+interface PersonRepository implements CrudRepository {
+ List findByLastname(String lastname);
+}
+----
+
diff --git a/src/main/java/org/springframework/data/keyvalue/annotation/KeySpace.java b/src/main/java/org/springframework/data/keyvalue/annotation/KeySpace.java
new file mode 100644
index 000000000..a4078f0f5
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/annotation/KeySpace.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014 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.keyvalue.annotation;
+
+import static java.lang.annotation.ElementType.*;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.data.annotation.Persistent;
+
+/**
+ * Marker interface for methods with {@link Persistent} annotations indicating the presence of a dedicated keyspace the
+ * entity should reside in. If present the value will be picked up for resolving the keyspace.
+ *
+ *
+ *
+ * Can also be directly used on types to indicate the keyspace.
+ *
+ *
+ *
+ * @KeySpace("persons")
+ * public class Foo {
+ *
+ * }
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = { METHOD, TYPE })
+public @interface KeySpace {
+
+ String value() default "";
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/AbstractKeyValueAdapter.java b/src/main/java/org/springframework/data/keyvalue/core/AbstractKeyValueAdapter.java
new file mode 100644
index 000000000..1c7198008
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/AbstractKeyValueAdapter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+
+/**
+ * Base implementation of {@link KeyValueAdapter} holds {@link QueryEngine} to delegate {@literal find} and
+ * {@literal count} execution to.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public abstract class AbstractKeyValueAdapter implements KeyValueAdapter {
+
+ private final QueryEngine extends KeyValueAdapter, ?, ?> engine;
+
+ /**
+ * Creates new {@link AbstractKeyValueAdapter} with using the default query engine.
+ */
+ protected AbstractKeyValueAdapter() {
+ this(null);
+ }
+
+ /**
+ * Creates new {@link AbstractKeyValueAdapter} with using the default query engine.
+ *
+ * @param engine will be defaulted to {@link SpelQueryEngine} if {@literal null}.
+ */
+ protected AbstractKeyValueAdapter(QueryEngine extends KeyValueAdapter, ?, ?> engine) {
+ this.engine = engine != null ? engine : new SpelQueryEngine();
+ this.engine.registerAdapter(this);
+ }
+
+ /**
+ * Get the {@link QueryEngine} used.
+ *
+ * @return
+ */
+ protected QueryEngine extends KeyValueAdapter, ?, ?> getQueryEngine() {
+ return engine;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueAdapter#find(org.springframework.data.keyvalue.core.query.KeyValueQuery, java.io.Serializable)
+ */
+ @Override
+ public Collection> find(KeyValueQuery> query, Serializable keyspace) {
+ return engine.execute(query, keyspace);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueAdapter#count(org.springframework.data.keyvalue.core.query.KeyValueQuery, java.io.Serializable)
+ */
+ @Override
+ public long count(KeyValueQuery> query, Serializable keyspace) {
+ return engine.count(query, keyspace);
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/CriteriaAccessor.java b/src/main/java/org/springframework/data/keyvalue/core/CriteriaAccessor.java
new file mode 100644
index 000000000..d1804ceb9
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/CriteriaAccessor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+
+/**
+ * Resolves the criteria object from given {@link KeyValueQuery}.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ * @param
+ */
+public interface CriteriaAccessor {
+
+ /**
+ * Checks and reads {@link KeyValueQuery#getCritieria()} of given {@link KeyValueQuery}. Might also apply additional
+ * transformation to match the desired type.
+ *
+ * @param query can be {@literal null}.
+ * @return the criteria extracted from the query.
+ * @throws IllegalArgumentException in case the criteria is not valid for usage with specific {@link CriteriaAccessor}
+ * .
+ */
+ T resolve(KeyValueQuery> query);
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/IdAccessor.java b/src/main/java/org/springframework/data/keyvalue/core/IdAccessor.java
new file mode 100644
index 000000000..ae282b989
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/IdAccessor.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.UUID;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.model.BeanWrapper;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public class IdAccessor {
+
+ private final PersistentEntity, ?> entity;
+ private final IdGenerator idGenerator;
+ private final BeanWrapper> wrapper;
+
+ @SuppressWarnings("rawtypes")
+ public IdAccessor(PersistentEntity, ? extends PersistentProperty> entity, BeanWrapper> wrapper) {
+ this(entity, wrapper, DefaultIdGenerator.INSTANCE);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public IdAccessor(PersistentEntity, ? extends PersistentProperty> entity, BeanWrapper> wrapper,
+ IdGenerator idGenerator) {
+
+ Assert.notNull(entity, "PersistentEntity must not be 'null'");
+ Assert.notNull(wrapper, "BeanWrapper must not be 'null'.");
+
+ this.idGenerator = idGenerator != null ? idGenerator : DefaultIdGenerator.INSTANCE;
+ this.entity = entity;
+ this.wrapper = wrapper;
+ }
+
+ public T getId() {
+
+ if (!entity.hasIdProperty()) {
+ throw new InvalidDataAccessApiUsageException(String.format("Cannot determine id for type %s", entity.getType()));
+ }
+
+ PersistentProperty> idProperty = entity.getIdProperty();
+ Object value = wrapper.getProperty(idProperty);
+
+ if (value == null) {
+ Serializable id = idGenerator.newIdForType(idProperty.getActualType());
+ wrapper.setProperty(idProperty, id);
+ return (T) id;
+ }
+ return (T) value;
+ }
+
+ /**
+ * @author Christoph Strobl
+ */
+ static enum DefaultIdGenerator implements IdGenerator {
+
+ INSTANCE;
+
+ @Override
+ public Serializable newIdForType(Class> idType) {
+
+ if (ClassUtils.isAssignable(String.class, idType)) {
+ return UUID.randomUUID().toString();
+ } else if (ClassUtils.isAssignable(Integer.class, idType)) {
+ try {
+ return SecureRandom.getInstance("NativePRNGBlocking");
+ } catch (NoSuchAlgorithmException e) {
+ throw new InvalidDataAccessApiUsageException("Could not create SecureRandom instance.", e);
+ }
+ } else if (ClassUtils.isAssignable(Long.class, idType)) {
+ try {
+ return SecureRandom.getInstance("NativePRNGBlocking").nextLong();
+ } catch (NoSuchAlgorithmException e) {
+ throw new InvalidDataAccessApiUsageException("Could not create SecureRandom instance.", e);
+ }
+ }
+
+ throw new InvalidDataAccessApiUsageException("Non gereratable id type....");
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/IdGenerator.java b/src/main/java/org/springframework/data/keyvalue/core/IdGenerator.java
new file mode 100644
index 000000000..91b0a0991
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/IdGenerator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+
+/**
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public interface IdGenerator {
+
+ Serializable newIdForType(Class> actualType);
+
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java
new file mode 100644
index 000000000..a2aee0a3f
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+
+/**
+ * {@link KeyValueAdapter} unifies access and shields the underlying key/value specific implementation.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public interface KeyValueAdapter extends DisposableBean {
+
+ /**
+ * Add object with given id to keyspace.
+ *
+ * @param id must not be {@literal null}.
+ * @param keyspace must not be {@literal null}.
+ * @return the item previously associated with the id.
+ */
+ Object put(Serializable id, Object item, Serializable keyspace);
+
+ /**
+ * Check if a object with given id exists in keyspace.
+ *
+ * @param id must not be {@literal null}.
+ * @param keyspace must not be {@literal null}.
+ * @return true if item of type with id exists.
+ */
+ boolean contains(Serializable id, Serializable keyspace);
+
+ /**
+ * Get the object with given id from keyspace.
+ *
+ * @param id must not be {@literal null}.
+ * @param keyspace must not be {@literal null}.
+ * @return {@literal null} in case no matching item exists.
+ */
+ Object get(Serializable id, Serializable keyspace);
+
+ /**
+ * Delete and return the obect with given type and id.
+ *
+ * @param id must not be {@literal null}.
+ * @param keyspace must not be {@literal null}.
+ * @return {@literal null} if object could not be found
+ */
+ Object delete(Serializable id, Serializable keyspace);
+
+ /**
+ * Get all elements for given keyspace.
+ *
+ * @param keyspace must not be {@literal null}.
+ * @return empty {@link Collection} if nothing found.
+ */
+ Collection> getAllOf(Serializable keyspace);
+
+ /**
+ * Remove all objects of given type.
+ *
+ * @param keyspace must not be {@literal null}.
+ */
+ void deleteAllOf(Serializable keyspace);
+
+ /**
+ * Removes all objects.
+ */
+ void clear();
+
+ /**
+ * Find all matching objects within {@literal keyspace}.
+ *
+ * @param query
+ * @param keyspace must not be {@literal null}.
+ * @return empty {@link Collection} if no match found.
+ */
+ Collection> find(KeyValueQuery> query, Serializable keyspace);
+
+ /**
+ * Count all matching objects within {@literal keyspace}.
+ *
+ * @param query
+ * @param keyspace must not be {@literal null}.
+ * @return
+ */
+ long count(KeyValueQuery> query, Serializable keyspace);
+
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueCallback.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueCallback.java
new file mode 100644
index 000000000..b30f49d2f
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueCallback.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+/**
+ * Generic callback interface for code that operates on a {@link KeyValueAdapter}. This is particularly useful for
+ * delegating code that needs to work closely on the underlying key/value store implementation.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ * @param
+ */
+public interface KeyValueCallback {
+
+ /**
+ * Gets called by {@code KeyValueTemplate#execute(KeyValueCallback)}. Allows for returning a result object created
+ * within the callback, i.e. a domain object or a collection of domain objects.
+ *
+ * @param adapter
+ * @return
+ */
+ T doInKeyValue(KeyValueAdapter adapter);
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueOperations.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueOperations.java
new file mode 100644
index 000000000..c3fd2022b
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueOperations.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.keyvalue.annotation.KeySpace;
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+import org.springframework.data.mapping.context.MappingContext;
+
+/**
+ * Interface that specifies a basic set of key/value operations. Implemented by {@link KeyValueTemplate}.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public interface KeyValueOperations extends DisposableBean {
+
+ /**
+ * Add given object.
+ * Object needs to have id property to which a generated value will be assigned.
+ *
+ * @param objectToInsert
+ * @return
+ */
+ T insert(T objectToInsert);
+
+ /**
+ * Add object with given id.
+ *
+ * @param id must not be {@literal null}.
+ * @param objectToInsert must not be {@literal null}.
+ */
+ void insert(Serializable id, Object objectToInsert);
+
+ /**
+ * Get all elements of given type.
+ * Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type.
+ *
+ * @param type must not be {@literal null}.
+ * @return empty collection if no elements found.
+ */
+ List findAll(Class type);
+
+ /**
+ * Get all elements ordered by sort. Respects {@link KeySpace} if present and therefore returns all elements that can
+ * be assigned to requested type.
+ *
+ * @param sort must not be {@literal null}.
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ List findAll(Sort sort, Class type);
+
+ /**
+ * Get element of given type with given id.
+ * Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type.
+ *
+ * @param id must not be {@literal null}.
+ * @param type must not be {@literal null}.
+ * @return null if not found.
+ */
+ T findById(Serializable id, Class type);
+
+ /**
+ * Execute operation against underlying store.
+ *
+ * @param action must not be {@literal null}.
+ * @return
+ */
+ T execute(KeyValueCallback action);
+
+ /**
+ * Get all elements matching the given query.
+ * Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type..
+ *
+ * @param query must not be {@literal null}.
+ * @param type must not be {@literal null}.
+ * @return empty collection if no match found.
+ */
+ List find(KeyValueQuery> query, Class type);
+
+ /**
+ * Get all elements in given range.
+ * Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type.
+ *
+ * @param offset
+ * @param rows
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ List findInRange(int offset, int rows, Class type);
+
+ /**
+ * Get all elements in given range ordered by sort.
+ * Respects {@link KeySpace} if present and therefore returns all elements that can be assigned to requested type.
+ *
+ * @param offset
+ * @param rows
+ * @param sort
+ * @param type
+ * @return
+ */
+ List findInRange(int offset, int rows, Sort sort, Class type);
+
+ /**
+ * @param objectToUpdate must not be {@literal null}.
+ */
+ void update(Object objectToUpdate);
+
+ /**
+ * @param id must not be {@literal null}.
+ * @param objectToUpdate must not be {@literal null}.
+ */
+ void update(Serializable id, Object objectToUpdate);
+
+ /**
+ * Remove all elements of type.
+ * Respects {@link KeySpace} if present and therefore removes all elements that can be assigned to requested type.
+ *
+ * @param type must not be {@literal null}.
+ */
+ void delete(Class> type);
+
+ /**
+ * @param objectToDelete must not be {@literal null}.
+ * @return
+ */
+ T delete(T objectToDelete);
+
+ /**
+ * Delete item of type with given id.
+ *
+ * @param id must not be {@literal null}.
+ * @param type must not be {@literal null}.
+ * @return the deleted item or {@literal null} if no match found.
+ */
+ T delete(Serializable id, Class type);
+
+ /**
+ * Total number of elements with given type available.
+ * Respects {@link KeySpace} if present and therefore counts all elements that can be assigned to requested type.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ long count(Class> type);
+
+ /**
+ * Total number of elements matching given query.
+ * Respects {@link KeySpace} if present and therefore counts all elements that can be assigned to requested type.
+ *
+ * @param query
+ * @param type
+ * @return
+ */
+ long count(KeyValueQuery> query, Class> type);
+
+ /**
+ * @return mapping context in use.
+ */
+ MappingContext, ?> getMappingContext();
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java
new file mode 100644
index 000000000..f50f2daa4
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2014 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.keyvalue.core;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.annotation.Persistent;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.keyvalue.annotation.KeySpace;
+import org.springframework.data.keyvalue.core.MetaAnnotationUtils.AnnotationDescriptor;
+import org.springframework.data.keyvalue.core.mapping.BasicMappingContext;
+import org.springframework.data.keyvalue.core.query.KeyValueQuery;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mapping.model.BeanWrapper;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Basic implementation of {@link KeyValueOperations}.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ */
+public class KeyValueTemplate implements KeyValueOperations {
+
+ private final KeyValueAdapter adapter;
+ private ConcurrentHashMap, String> keySpaceCache = new ConcurrentHashMap, String>();
+
+ @SuppressWarnings("rawtypes")//
+ private MappingContext extends PersistentEntity, ? extends PersistentProperty>, ? extends PersistentProperty>> mappingContext;
+
+ /**
+ * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} with a default
+ * {@link BasicMappingContext}.
+ *
+ * @param adapter must not be {@literal null}.
+ */
+ public KeyValueTemplate(KeyValueAdapter adapter) {
+ this(adapter, new BasicMappingContext());
+ }
+
+ /**
+ * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link MappingContext}.
+ *
+ * @param adapter must not be {@literal null}.
+ * @param mappingContext must not be {@literal null}.
+ */
+ @SuppressWarnings("rawtypes")
+ public KeyValueTemplate(
+ KeyValueAdapter adapter,
+ MappingContext extends PersistentEntity, ? extends PersistentProperty>, ? extends PersistentProperty>> mappingContext) {
+
+ Assert.notNull(adapter, "Adapter must not be 'null' when intializing KeyValueTemplate.");
+ Assert.notNull(mappingContext, "MappingContext must not be 'null' when intializing KeyValueTemplate.");
+
+ this.adapter = adapter;
+ this.mappingContext = mappingContext;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#insert(java.lang.Object)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public T insert(T objectToInsert) {
+
+ PersistentEntity, ? extends PersistentProperty> entity = this.mappingContext.getPersistentEntity(ClassUtils
+ .getUserClass(objectToInsert));
+
+ Serializable id = new IdAccessor(entity, BeanWrapper.create(objectToInsert, null)).getId();
+
+ insert(id, objectToInsert);
+ return objectToInsert;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#insert(java.io.Serializable, java.lang.Object)
+ */
+ @Override
+ public void insert(final Serializable id, final Object objectToInsert) {
+
+ Assert.notNull(id, "Id for object to be inserted must not be 'null'.");
+ Assert.notNull(objectToInsert, "Object to be inserted must not be 'null'.");
+
+ execute(new KeyValueCallback() {
+
+ @Override
+ public Void doInKeyValue(KeyValueAdapter adapter) {
+
+ String typeKey = resolveKeySpace(objectToInsert.getClass());
+
+ if (adapter.contains(id, typeKey)) {
+ throw new InvalidDataAccessApiUsageException("Cannot insert existing object. Please use update.");
+ }
+
+ adapter.put(id, objectToInsert, typeKey);
+ return null;
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#update(java.lang.Object)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void update(Object objectToUpdate) {
+
+ PersistentEntity, ? extends PersistentProperty> entity = this.mappingContext.getPersistentEntity(ClassUtils
+ .getUserClass(objectToUpdate));
+
+ if (!entity.hasIdProperty()) {
+ throw new InvalidDataAccessApiUsageException(String.format("Cannot determine id for type %s",
+ ClassUtils.getUserClass(objectToUpdate)));
+ }
+
+ Serializable id = BeanWrapper.create(objectToUpdate, null).getProperty(entity.getIdProperty(), Serializable.class);
+ update(id, objectToUpdate);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#update(java.io.Serializable, java.lang.Object)
+ */
+ @Override
+ public void update(final Serializable id, final Object objectToUpdate) {
+
+ Assert.notNull(id, "Id for object to be inserted must not be 'null'.");
+ Assert.notNull(objectToUpdate, "Object to be updated must not be 'null'. Use delete to remove.");
+
+ execute(new KeyValueCallback() {
+
+ @Override
+ public Void doInKeyValue(KeyValueAdapter adapter) {
+ adapter.put(id, objectToUpdate, resolveKeySpace(objectToUpdate.getClass()));
+ return null;
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#findAllOf(java.lang.Class)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public List findAll(final Class type) {
+
+ Assert.notNull(type, "Type to fetch must not be 'null'.");
+
+ return execute(new KeyValueCallback>() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List doInKeyValue(KeyValueAdapter adapter) {
+
+ Collection> x = adapter.getAllOf(resolveKeySpace(type));
+
+ if (getKeySpace(type) == null) {
+ return new ArrayList((Collection) x);
+ }
+
+ ArrayList filtered = new ArrayList();
+ for (Object candidate : x) {
+ if (typeCheck(type, candidate)) {
+ filtered.add((T) candidate);
+ }
+ }
+
+ return filtered;
+ }
+ });
+ }
+
+ /*
+ *(non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#findById(java.io.Serializable, java.lang.Class)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public T findById(final Serializable id, final Class type) {
+
+ Assert.notNull(id, "Id for object to be inserted must not be 'null'.");
+ Assert.notNull(type, "Type to fetch must not be 'null'.");
+
+ return execute(new KeyValueCallback() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T doInKeyValue(KeyValueAdapter adapter) {
+
+ Object result = adapter.get(id, resolveKeySpace(type));
+
+ if (result == null || getKeySpace(type) == null || typeCheck(type, result)) {
+ return (T) result;
+ }
+
+ return null;
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#delete(java.lang.Class)
+ */
+ @Override
+ public void delete(final Class> type) {
+
+ Assert.notNull(type, "Type to delete must not be 'null'.");
+
+ final String typeKey = resolveKeySpace(type);
+
+ execute(new KeyValueCallback() {
+
+ @Override
+ public Void doInKeyValue(KeyValueAdapter adapter) {
+
+ adapter.deleteAllOf(typeKey);
+ return null;
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#delete(java.lang.Object)
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ public T delete(T objectToDelete) {
+
+ Class type = (Class) ClassUtils.getUserClass(objectToDelete);
+ PersistentEntity, ? extends PersistentProperty> entity = this.mappingContext.getPersistentEntity(type);
+
+ Serializable id = new IdAccessor(entity, BeanWrapper.create(objectToDelete, null)).getId();
+ return delete(id, type);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#delete(java.io.Serializable, java.lang.Class)
+ */
+ @Override
+ public T delete(final Serializable id, final Class type) {
+
+ Assert.notNull(id, "Id for object to be inserted must not be 'null'.");
+ Assert.notNull(type, "Type to delete must not be 'null'.");
+
+ return execute(new KeyValueCallback() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T doInKeyValue(KeyValueAdapter adapter) {
+ return (T) adapter.delete(id, resolveKeySpace(type));
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#count(java.lang.Class)
+ */
+ @Override
+ public long count(Class> type) {
+
+ Assert.notNull(type, "Type for count must not be null!");
+ return findAll(type).size();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#execute(org.springframework.data.keyvalue.core.KeyValueCallback)
+ */
+ @Override
+ public T execute(KeyValueCallback action) {
+
+ Assert.notNull(action, "KeyValueCallback must not be 'null'.");
+
+ try {
+ return action.doInKeyValue(this.adapter);
+ } catch (RuntimeException e) {
+
+ // TODO: potentially convert runtime exception?
+ throw e;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#find(org.springframework.data.keyvalue.core.query.KeyValueQuery, java.lang.Class)
+ */
+ @Override
+ public List find(final KeyValueQuery> query, final Class type) {
+
+ return execute(new KeyValueCallback>() {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List doInKeyValue(KeyValueAdapter adapter) {
+
+ Collection> result = adapter.find(query, resolveKeySpace(type));
+
+ if (getKeySpace(type) == null) {
+ return new ArrayList((Collection) result);
+ }
+
+ ArrayList filtered = new ArrayList();
+ for (Object candidate : result) {
+ if (typeCheck(type, candidate)) {
+ filtered.add((T) candidate);
+ }
+ }
+
+ return filtered;
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#findAllOf(org.springframework.data.domain.Sort, java.lang.Class)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public List findAll(Sort sort, Class type) {
+ return find(new KeyValueQuery(sort), type);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#findInRange(int, int, java.lang.Class)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public List findInRange(int offset, int rows, Class type) {
+ return find(new KeyValueQuery().skip(offset).limit(rows), type);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#findInRange(int, int, org.springframework.data.domain.Sort, java.lang.Class)
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public List findInRange(int offset, int rows, Sort sort, Class type) {
+ return find(new KeyValueQuery(sort).skip(offset).limit(rows), type);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#count(org.springframework.data.keyvalue.core.query.KeyValueQuery, java.lang.Class)
+ */
+ @Override
+ public long count(final KeyValueQuery> query, final Class> type) {
+
+ return execute(new KeyValueCallback() {
+
+ @Override
+ public Long doInKeyValue(KeyValueAdapter adapter) {
+ return adapter.count(query, resolveKeySpace(type));
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.keyvalue.core.KeyValueOperations#getMappingContext()
+ */
+ @Override
+ public MappingContext, ?> getMappingContext() {
+ return this.mappingContext;
+ }
+
+ protected String resolveKeySpace(Class> type) {
+
+ Class> userClass = ClassUtils.getUserClass(type);
+
+ String potentialAlias = keySpaceCache.get(userClass);
+
+ if (potentialAlias != null) {
+ return potentialAlias;
+ }
+
+ String keySpaceString = null;
+ Object keySpace = getKeySpace(type);
+ if (keySpace != null) {
+ keySpaceString = keySpace.toString();
+ }
+
+ if (!StringUtils.hasText(keySpaceString)) {
+ keySpaceString = userClass.getName();
+ }
+
+ keySpaceCache.put(userClass, keySpaceString);
+ return keySpaceString;
+ }
+
+ /**
+ * Looks up {@link Persistent} when used as meta annotation to find the {@link KeySpace} attribute.
+ *
+ * @return
+ * @since 1.10
+ */
+
+ Object getKeySpace(Class> type) {
+
+ KeySpace keyspace = AnnotationUtils.findAnnotation(type, KeySpace.class);
+ if (keyspace != null) {
+ return AnnotationUtils.getValue(keyspace);
+ }
+
+ AnnotationDescriptor descriptor = MetaAnnotationUtils.findAnnotationDescriptor(type, Persistent.class);
+
+ if (descriptor != null && descriptor.getComposedAnnotation() != null) {
+
+ Annotation composed = descriptor.getComposedAnnotation();
+
+ for (Method method : descriptor.getComposedAnnotationType().getDeclaredMethods()) {
+
+ keyspace = AnnotationUtils.findAnnotation(method, KeySpace.class);
+
+ if (keyspace != null) {
+ return AnnotationUtils.getValue(composed, method.getName());
+ }
+ }
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.beans.factory.DisposableBean#destroy()
+ */
+ @Override
+ public void destroy() throws Exception {
+ this.adapter.clear();
+ }
+
+ private boolean typeCheck(Class> requiredType, Object candidate) {
+
+ if (candidate == null) {
+ return true;
+ }
+ return ClassUtils.isAssignable(requiredType, candidate.getClass());
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/keyvalue/core/MetaAnnotationUtils.java b/src/main/java/org/springframework/data/keyvalue/core/MetaAnnotationUtils.java
new file mode 100644
index 000000000..640540537
--- /dev/null
+++ b/src/main/java/org/springframework/data/keyvalue/core/MetaAnnotationUtils.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2002-2014 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.keyvalue.core;
+
+import java.lang.annotation.Annotation;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * {@code MetaAnnotationUtils} is a collection of utility methods that complements the standard support already
+ * available in {@link AnnotationUtils}.
+ *
+ * Whereas {@code AnnotationUtils} provides utilities for getting or finding an annotation,
+ * {@code MetaAnnotationUtils} goes a step further by providing support for determining the root class on which
+ * an annotation is declared, either directly or indirectly via a composed
+ * annotation. This additional information is encapsulated in an {@link AnnotationDescriptor}.
+ *
+ * The additional information provided by an {@code AnnotationDescriptor} is required by the
+ * Spring TestContext Framework in order to be able to support class hierarchy traversals for annotations such
+ * as {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration},
+ * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}, and
+ * {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles} which offer support for merging and
+ * overriding various inherited annotation attributes (e.g.,
+ * {@link org.springframework.test.context.ContextConfiguration#inheritLocations}).
+ *
+ * @author Sam Brannen
+ * @since 4.0
+ * @see AnnotationUtils
+ * @see AnnotationDescriptor
+ */
+public abstract class MetaAnnotationUtils {
+
+ private MetaAnnotationUtils() {
+ /* no-op */
+ }
+
+ /**
+ * Find the {@link AnnotationDescriptor} for the supplied {@code annotationType} on the supplied {@link Class},
+ * traversing its annotations and superclasses if no annotation can be found on the given class itself.
+ *
+ * This method explicitly handles class-level annotations which are not declared as
+ * {@linkplain java.lang.annotation.Inherited inherited} as
+ * well as meta-annotations.
+ *
+ * The algorithm operates as follows:
+ *
+ *
Search for the annotation on the given class and return a corresponding {@code AnnotationDescriptor} if found.
+ *
Recursively search through all annotations that the given class declares.
+ *
Recursively search through the superclass hierarchy of the given class.
+ *
+ *
+ * In this context, the term recursively means that the search process continues by returning to step #1 with
+ * the current annotation or superclass as the class to look for annotations on.
+ *
+ * If the supplied {@code clazz} is an interface, only the interface itself will be checked; the inheritance hierarchy
+ * for interfaces will not be traversed.
+ *
+ * @param clazz the class to look for annotations on
+ * @param annotationType the type of annotation to look for
+ * @return the corresponding annotation descriptor if the annotation was found; otherwise {@code null}
+ * @see AnnotationUtils#findAnnotationDeclaringClass(Class, Class)
+ * @see #findAnnotationDescriptorForTypes(Class, Class...)
+ */
+ public static AnnotationDescriptor findAnnotationDescriptor(Class> clazz,
+ Class annotationType) {
+ return findAnnotationDescriptor(clazz, new HashSet(), annotationType);
+ }
+
+ /**
+ * Perform the search algorithm for {@link #findAnnotationDescriptor(Class, Class)}, avoiding endless recursion by
+ * tracking which annotations have already been visited.
+ *
+ * @param clazz the class to look for annotations on
+ * @param visited the set of annotations that have already been visited
+ * @param annotationType the type of annotation to look for
+ * @return the corresponding annotation descriptor if the annotation was found; otherwise {@code null}
+ */
+ private static AnnotationDescriptor findAnnotationDescriptor(Class> clazz,
+ Set visited, Class annotationType) {
+
+ Assert.notNull(annotationType, "Annotation type must not be null");
+
+ if (clazz == null || clazz.equals(Object.class)) {
+ return null;
+ }
+
+ // Declared locally?
+ if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) {
+ return new AnnotationDescriptor(clazz, clazz.getAnnotation(annotationType));
+ }
+
+ // Declared on a composed annotation (i.e., as a meta-annotation)?
+ for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) {
+ if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) {
+ AnnotationDescriptor descriptor = findAnnotationDescriptor(composedAnnotation.annotationType(), visited,
+ annotationType);
+ if (descriptor != null) {
+ return new AnnotationDescriptor(clazz, descriptor.getDeclaringClass(), composedAnnotation,
+ descriptor.getAnnotation());
+ }
+ }
+ }
+
+ // Declared on a superclass?
+ return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
+ }
+
+ /**
+ * Find the {@link UntypedAnnotationDescriptor} for the first {@link Class} in the inheritance hierarchy of the
+ * specified {@code clazz} (including the specified {@code clazz} itself) which declares at least one of the specified
+ * {@code annotationTypes}.
+ *
+ * This method traverses the annotations and superclasses of the specified {@code clazz} if no annotation can be found
+ * on the given class itself.
+ *
+ * This method explicitly handles class-level annotations which are not declared as
+ * {@linkplain java.lang.annotation.Inherited inherited} as
+ * well as meta-annotations.
+ *
+ * The algorithm operates as follows:
+ *
+ *
Search for a local declaration of one of the annotation types on the given class and return a corresponding
+ * {@code UntypedAnnotationDescriptor} if found.
+ *
Recursively search through all annotations that the given class declares.
+ *
Recursively search through the superclass hierarchy of the given class.
+ *
+ *
+ * In this context, the term recursively means that the search process continues by returning to step #1 with
+ * the current annotation or superclass as the class to look for annotations on.
+ *
+ * If the supplied {@code clazz} is an interface, only the interface itself will be checked; the inheritance hierarchy
+ * for interfaces will not be traversed.
+ *
+ * @param clazz the class to look for annotations on
+ * @param annotationTypes the types of annotations to look for
+ * @return the corresponding annotation descriptor if one of the annotations was found; otherwise {@code null}
+ * @see AnnotationUtils#findAnnotationDeclaringClassForTypes(java.util.List, Class)
+ * @see #findAnnotationDescriptor(Class, Class)
+ */
+ @SuppressWarnings("unchecked")
+ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class> clazz,
+ Class extends Annotation>... annotationTypes) {
+ return findAnnotationDescriptorForTypes(clazz, new HashSet(), annotationTypes);
+ }
+
+ /**
+ * Perform the search algorithm for {@link #findAnnotationDescriptorForTypes(Class, Class...)}, avoiding endless
+ * recursion by tracking which annotations have already been visited.
+ *
+ * @param clazz the class to look for annotations on
+ * @param visited the set of annotations that have already been visited
+ * @param annotationTypes the types of annotations to look for
+ * @return the corresponding annotation descriptor if one of the annotations was found; otherwise {@code null}
+ */
+ @SuppressWarnings("unchecked")
+ private static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class> clazz, Set visited,
+ Class extends Annotation>... annotationTypes) {
+
+ assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty");
+
+ if (clazz == null || clazz.equals(Object.class)) {
+ return null;
+ }
+
+ // Declared locally?
+ for (Class extends Annotation> annotationType : annotationTypes) {
+ if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) {
+ return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType));
+ }
+ }
+
+ // Declared on a composed annotation (i.e., as a meta-annotation)?
+ for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) {
+ if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) {
+ UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(composedAnnotation.annotationType(),
+ visited, annotationTypes);
+ if (descriptor != null) {
+ return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(), composedAnnotation,
+ descriptor.getAnnotation());
+ }
+ }
+ }
+
+ // Declared on a superclass?
+ return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
+ }
+
+ /**
+ * Descriptor for an {@link Annotation}, including the {@linkplain #getDeclaringClass() class} on which the annotation
+ * is declared as well as the actual {@linkplain #getAnnotation() annotation} instance.
+ *
+ * If the annotation is used as a meta-annotation, the descriptor also includes the
+ * {@linkplain #getComposedAnnotation() composed annotation} on which the annotation is present. In such cases, the
+ * root declaring class is not directly annotated with the annotation but rather indirectly via the composed
+ * annotation.
+ *
+ * Given the following example, if we are searching for the {@code @Transactional} annotation on the
+ * {@code TransactionalTests} class, then the properties of the {@code AnnotationDescriptor} would be as follows.
+ *
+ *
rootDeclaringClass: {@code TransactionalTests} class object
+ *
declaringClass: {@code TransactionalTests} class object
+ *
composedAnnotation: {@code null}
+ *
annotation: instance of the {@code Transactional} annotation
+ *
+ *
+ *
+ * @Transactional
+ * @ContextConfiguration({ "/test-datasource.xml", "/repository-config.xml" })
+ * public class TransactionalTests {}
+ *
+ *
+ * Given the following example, if we are searching for the {@code @Transactional} annotation on the
+ * {@code UserRepositoryTests} class, then the properties of the {@code AnnotationDescriptor} would be as follows.
+ *
+ *
rootDeclaringClass: {@code UserRepositoryTests} class object
+ *
declaringClass: {@code RepositoryTests} class object
+ *
composedAnnotation: instance of the {@code RepositoryTests} annotation
+ *
annotation: instance of the {@code Transactional} annotation