From 1761255c2dfe4197d3741ffc445e93f03d625e74 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 14 May 2011 08:37:38 +0200 Subject: [PATCH] DATACMNS-37 - Enabled annotation based repository declaration model. Introduced an @RepositoryProxy annotation that can be used as alternative to extending Repository. Added necessary RepositoryMetadata implementation to introspect the annotation rather than the generic types. Enabled bean definition parsers to pick up annotation based repositories as well. --- .../data/repository/RepositoryProxy.java | 39 +++++++++ ...tractRepositoryConfigDefinitionParser.java | 87 ++++++++++++++++++- .../support/AnnotationRepositoryMetadata.java | 50 +++++++++++ .../support/DefaultRepositoryMetadata.java | 37 ++++---- .../support/RepositoryFactorySupport.java | 3 +- ...AnnotationRepositoryMetadataUnitTests.java | 65 ++++++++++++++ .../DefaultRepositoryMetadataUnitTests.java | 3 +- 7 files changed, 259 insertions(+), 25 deletions(-) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/RepositoryProxy.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AnnotationRepositoryMetadata.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AnnotationRepositoryMetadataUnitTests.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/RepositoryProxy.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/RepositoryProxy.java new file mode 100644 index 000000000..ab208f035 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/RepositoryProxy.java @@ -0,0 +1,39 @@ +package org.springframework.data.repository; + +import java.io.Serializable; +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 demarcate interfaces a repository proxy shall be created for. Annotating an interface with + * {@link RepositoryProxy} will cause the same behaviour as extending {@link Repository}. + * + * @see Repository + * @author Oliver Gierke + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface RepositoryProxy { + + /** + * The domain class the repository manages. Equivalent to the T type parameter in {@link Repository}. + * + * @see Repository + * @return + */ + Class domainClass(); + + /** + * The id class of the entity the repository manages. Equivalent to the ID type parameter in {@link Repository}. + * + * @see Repository + * @return + */ + Class idClass(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java index 5417b006d..bca6441ae 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/config/AbstractRepositoryConfigDefinitionParser.java @@ -19,6 +19,8 @@ import static org.springframework.beans.factory.support.BeanDefinitionReaderUtil import static org.springframework.data.repository.util.ClassUtils.*; import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; @@ -36,12 +38,14 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.RepositoryProxy; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -414,6 +418,7 @@ public abstract class AbstractRepositoryConfigDefinitionParserThe matching logic mirrors that of Class.isAnnotationPresent(). + * + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ + private static class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private final Class annotationType; + + private final boolean considerMetaAnnotations; + + + /** + * Create a new AnnotationTypeFilter for the given annotation type. + * This filter will also match meta-annotations. To disable the + * meta-annotation matching, use the constructor that accepts a + * 'considerMetaAnnotations' argument. The filter will + * not match interfaces. + * @param annotationType the annotation type to match + */ + public AnnotationTypeFilter(Class annotationType) { + this(annotationType, true); + } + + /** + * Create a new AnnotationTypeFilter for the given annotation type. + * The filter will not match interfaces. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + */ + public AnnotationTypeFilter(Class annotationType, boolean considerMetaAnnotations) { + this(annotationType, considerMetaAnnotations, false); + } + + /** + * Create a new {@link AnnotationTypeFilter} for the given annotation type. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + * @param considerInterfaces whether to also match interfaces + */ + public AnnotationTypeFilter(Class annotationType, boolean considerMetaAnnotations, boolean considerInterfaces) { + super(annotationType.isAnnotationPresent(Inherited.class), considerInterfaces); + this.annotationType = annotationType; + this.considerMetaAnnotations = considerMetaAnnotations; + } + + + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); + } + + @Override + protected Boolean matchSuperClass(String superClassName) { + if (Object.class.getName().equals(superClassName)) { + return Boolean.FALSE; + } + else if (superClassName.startsWith("java.")) { + try { + Class clazz = getClass().getClassLoader().loadClass(superClassName); + return (clazz.getAnnotation(this.annotationType) != null); + } + catch (ClassNotFoundException ex) { + // Class not found - can't determine a match that way. + } + } + return null; + } + } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AnnotationRepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AnnotationRepositoryMetadata.java new file mode 100644 index 000000000..0f48c783c --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AnnotationRepositoryMetadata.java @@ -0,0 +1,50 @@ +package org.springframework.data.repository.support; + +import org.springframework.data.repository.RepositoryProxy; +import org.springframework.util.Assert; + +/** + * {@link RepositoryMetadata} implementation inspecting the given repository interface for a {@link RepositoryProxy} + * annotation. + * + * @author Oliver Gierke + */ +public class AnnotationRepositoryMetadata implements RepositoryMetadata { + + private static final String NO_ANNOTATION_FOUND = String.format("Interface must be annotated with @%s!", + RepositoryProxy.class.getName()); + + private final Class repositoryInterface; + + public AnnotationRepositoryMetadata(Class repositoryInterface) { + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + Assert.isTrue(repositoryInterface.isAnnotationPresent(RepositoryProxy.class), NO_ANNOTATION_FOUND); + this.repositoryInterface = repositoryInterface; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() + */ + public Class getIdClass() { + RepositoryProxy annotation = repositoryInterface.getAnnotation(RepositoryProxy.class); + return annotation == null ? null : annotation.idClass(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() + */ + public Class getDomainClass() { + RepositoryProxy annotation = repositoryInterface.getAnnotation(RepositoryProxy.class); + return annotation == null ? null : annotation.domainClass(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getRepositoryInterface() + */ + public Class getRepositoryInterface() { + return repositoryInterface; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java index de72a340b..816415845 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java @@ -20,10 +20,10 @@ import static org.springframework.core.GenericTypeResolver.*; import org.springframework.data.repository.Repository; import org.springframework.util.Assert; - /** - * Default implementation of {@link RepositoryMetadata}. - * + * Default implementation of {@link RepositoryMetadata}. Will inspect generic types of + * {@link Repository} to find out about domain and id class. + * * @author Oliver Gierke */ public class DefaultRepositoryMetadata implements RepositoryMetadata { @@ -33,23 +33,22 @@ public class DefaultRepositoryMetadata implements RepositoryMetadata { /** * Creates a new {@link DefaultRepositoryMetadata} for the given repository - * interface and repository base class. + * interface. * * @param repositoryInterface */ public DefaultRepositoryMetadata(Class repositoryInterface) { Assert.notNull(repositoryInterface); + Assert.isTrue(Repository.class.isAssignableFrom(repositoryInterface)); this.repositoryInterface = repositoryInterface; } /* - * (non-Javadoc) - * - * @see org.springframework.data.repository.support.RepositoryMetadata# - * getRepositoryInterface() - */ + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getRepositoryInterface() + */ public Class getRepositoryInterface() { return repositoryInterface; @@ -57,14 +56,11 @@ public class DefaultRepositoryMetadata implements RepositoryMetadata { /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositoryMetadata#getDomainClass - * () - */ + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getDomainClass() + */ public Class getDomainClass() { - + Class[] arguments = resolveTypeArguments(repositoryInterface, Repository.class); return arguments == null ? null : arguments[0]; @@ -72,12 +68,9 @@ public class DefaultRepositoryMetadata implements RepositoryMetadata { /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositoryMetadata#getIdClass - * () - */ + * (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryMetadata#getIdClass() + */ public Class getIdClass() { Class[] arguments = diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java index 883033a9e..bbe9a514e 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -153,7 +153,8 @@ public abstract class RepositoryFactorySupport { * @return */ RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { - return new DefaultRepositoryMetadata(repositoryInterface); + return Repository.class.isAssignableFrom(repositoryInterface) ? new DefaultRepositoryMetadata(repositoryInterface) + : new AnnotationRepositoryMetadata(repositoryInterface); } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AnnotationRepositoryMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AnnotationRepositoryMetadataUnitTests.java new file mode 100644 index 000000000..9ca5d732b --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AnnotationRepositoryMetadataUnitTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.repository.RepositoryProxy; + + +/** + * Unit tests for {@link DefaultRepositoryMetadata}. + * + * @author Oliver Gierke + */ +public class AnnotationRepositoryMetadataUnitTests { + + @Test + public void handlesRepositoryProxyAnnotationCorrectly() { + + RepositoryMetadata metadata = new AnnotationRepositoryMetadata(AnnotatedRepository.class); + assertEquals(User.class, metadata.getDomainClass()); + assertEquals(Integer.class, metadata.getIdClass()); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsUnannotatedInterface() { + + new AnnotationRepositoryMetadata(UnannotatedRepository.class); + } + + @SuppressWarnings("unused") + private class User { + + private String firstname; + + + public String getAddress() { + + return null; + } + } + + @RepositoryProxy(domainClass = User.class, idClass = Integer.class) + interface AnnotatedRepository { + + } + + interface UnannotatedRepository { + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java index 49c99a47d..117993286 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.repository.support; +import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.io.Serializable; @@ -23,6 +24,7 @@ import org.junit.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.RepositoryProxy; import org.springframework.data.repository.util.ClassUtils; @@ -73,7 +75,6 @@ public class DefaultRepositoryMetadataUnitTests { assertEquals(Integer.class, metadata.getIdClass()); } - @SuppressWarnings("unused") private class User {