From 80359c4d07de563d9083adf11694fd169f1e3449 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 11 Mar 2011 15:59:15 +0100 Subject: [PATCH] DATAJPA-19 - Added Hades extensions module. Added DomainClassPropertyEditor and DomainClassConverter to automatically bind domain classes to Spring MVC controller methods. Same applies to PageableArgumentResolver. Polished up some generics. Introduced RepositoryFactoryInformation interface being implemented by RepositoryFactorySupport that allows the extension components to get access to the raw factories, access EntityInformation of it and then find out about the repository interface to lookup the actual repository instance. --- pom.xml | 1 + .../data/repository/query/QueryMethod.java | 2 +- .../support/AbstractEntityInformation.java | 6 +- .../repository/support/EntityInformation.java | 13 +- .../support/PersistableEntityInformation.java | 21 +- .../support/RepositoryFactoryBeanSupport.java | 25 +- .../support/RepositoryFactoryInformation.java | 45 +++ .../support/RepositoryFactorySupport.java | 27 +- ...sactionalRepositoryFactoryBeanSupport.java | 6 +- .../AbstractEntityInformationUnitTests.java | 17 +- ...PersistableEntityInformationUnitTests.java | 8 +- .../RepositoryFactorySupportUnitTests.java | 10 + spring-data-commons-extensions/pom.xml | 98 ++++++ .../beans/DomainClassPropertyEditor.java | 180 ++++++++++ .../DomainClassPropertyEditorRegistrar.java | 115 ++++++ .../converter/DomainClassConverter.java | 159 +++++++++ .../web/PageableArgumentResolver.java | 327 ++++++++++++++++++ .../data/extensions/web/PageableDefaults.java | 50 +++ ...ClassPropertyEditorRegistrarUnitTests.java | 148 ++++++++ .../DomainClassPropertyEditorUnitTests.java | 182 ++++++++++ .../DomainClassConverterUnitTests.java | 192 ++++++++++ .../PageableArgumentResolverUnitTests.java | 193 +++++++++++ spring-data-commons-extensions/template.mf | 16 + spring-data-commons-parent/pom.xml | 10 + 24 files changed, 1829 insertions(+), 22 deletions(-) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryInformation.java create mode 100644 spring-data-commons-extensions/pom.xml create mode 100644 spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditor.java create mode 100644 spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrar.java create mode 100644 spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/converter/DomainClassConverter.java create mode 100644 spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableArgumentResolver.java create mode 100644 spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableDefaults.java create mode 100644 spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrarUnitTests.java create mode 100644 spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorUnitTests.java create mode 100644 spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/conversion/DomainClassConverterUnitTests.java create mode 100644 spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/web/PageableArgumentResolverUnitTests.java create mode 100644 spring-data-commons-extensions/template.mf diff --git a/pom.xml b/pom.xml index d83db4c97..ddeb43d54 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ spring-data-commons-parent spring-data-commons-core spring-data-commons-aspects + spring-data-commons-extensions diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java index f05f5e720..7907550a4 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -89,7 +89,7 @@ public class QueryMethod { } - public EntityMetadata getEntityMetadata() { + public EntityMetadata getEntityInformation() { return new EntityMetadata() { diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java index fd715e23e..2a6ed38dc 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.support; +import java.io.Serializable; + import org.springframework.util.Assert; @@ -24,8 +26,8 @@ import org.springframework.util.Assert; * * @author Oliver Gierke */ -public abstract class AbstractEntityInformation implements - EntityInformation { +public abstract class AbstractEntityInformation implements + EntityInformation { private final Class domainClass; diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java index 33797d33f..8ccf3b538 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java @@ -15,13 +15,15 @@ */ package org.springframework.data.repository.support; +import java.io.Serializable; + /** * Extension of {@link EntityMetadata} to add functionality to query information * of entity instances. * * @author Oliver Gierke */ -public interface EntityInformation extends EntityMetadata { +public interface EntityInformation extends EntityMetadata { /** * Returns whether the given entity is considered to be new. @@ -38,5 +40,12 @@ public interface EntityInformation extends EntityMetadata { * @param entity must never be {@literal null} * @return */ - Object getId(T entity); + ID getId(T entity); + + /** + * Returns the type of the id of the entity. + * + * @return + */ + Class getIdType(); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java index ffb873a39..d99c74314 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java @@ -15,6 +15,9 @@ */ package org.springframework.data.repository.support; +import java.io.Serializable; + +import org.springframework.core.GenericTypeResolver; import org.springframework.data.domain.Persistable; @@ -25,18 +28,21 @@ import org.springframework.data.domain.Persistable; * * @author Oliver Gierke */ -@SuppressWarnings("rawtypes") -public class PersistableEntityInformation extends - AbstractEntityInformation { +public class PersistableEntityInformation, ID extends Serializable> extends + AbstractEntityInformation { + + private Class idClass; /** * Creates a new {@link PersistableEntityInformation}. * * @param domainClass */ + @SuppressWarnings("unchecked") public PersistableEntityInformation(Class domainClass) { super(domainClass); + this.idClass = (Class) GenericTypeResolver.resolveTypeArgument(domainClass, Persistable.class); } @@ -61,8 +67,15 @@ public class PersistableEntityInformation extends * org.springframework.data.repository.support.IdAware#getId(java.lang.Object * ) */ - public Object getId(T entity) { + public ID getId(T entity) { return entity.getId(); } + + /* (non-Javadoc) + * @see org.springframework.data.repository.support.EntityInformation#getIdType() + */ + public Class getIdType() { + return this.idClass; + } } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java index 00ab4b49b..a576501b9 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.support; +import java.io.Serializable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Required; @@ -31,8 +33,8 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @param the type of the repository */ -public abstract class RepositoryFactoryBeanSupport> - implements FactoryBean, InitializingBean { +public abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> + implements InitializingBean, RepositoryFactoryInformation, FactoryBean { private RepositoryFactorySupport factory; @@ -75,6 +77,25 @@ public abstract class RepositoryFactoryBeanSupport> this.customImplementation = customImplementation; } + + /* (non-Javadoc) + * @see org.springframework.data.repository.support.EntityMetadataProvider#getEntityMetadata() + */ + public EntityInformation getEntityInformation() { + + RepositoryMetadata repositoryMetadata = factory.getRepositoryMetadata(repositoryInterface); + return (EntityInformation) factory.getEntityInformation(repositoryMetadata.getDomainClass()); + } + + + /* (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryFactoryInformation#getRepositoryInterface() + */ + @Override + public Class getRepositoryInterface() { + + return repositoryInterface; + } /* * (non-Javadoc) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryInformation.java new file mode 100644 index 000000000..1a2a0831c --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryInformation.java @@ -0,0 +1,45 @@ +/* + * 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 java.io.Serializable; + +import org.springframework.data.repository.Repository; + + +/** + * Interface for components that can provide {@link EntityInformation} this + * interface + * + * @author Oliver Gierke + */ +public interface RepositoryFactoryInformation { + + /** + * Returns {@link EntityInformation} the repository factory is using. + * + * @return + */ + EntityInformation getEntityInformation(); + + + /** + * Returns the interface of the {@link Repository} the factory will create. + * + * @return + */ + Class> getRepositoryInterface(); +} 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 ecb978219..c6b8477be 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 @@ -17,6 +17,7 @@ package org.springframework.data.repository.support; import static org.springframework.util.ReflectionUtils.*; +import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -124,8 +125,7 @@ public abstract class RepositoryFactorySupport { Object customImplementation) { RepositoryMetadata metadata = - new DefaultRepositoryMetadata(repositoryInterface, - getRepositoryBaseClass(repositoryInterface)); + getRepositoryMetadata(repositoryInterface); validate(metadata, customImplementation); @@ -145,6 +145,29 @@ public abstract class RepositoryFactorySupport { return (T) result.getProxy(); } + + + /** + * Returns the {@link RepositoryMetadata} for the given repository interface. + * + * @param repositoryInterface + * @return + */ + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + + return new DefaultRepositoryMetadata(repositoryInterface, getRepositoryBaseClass(repositoryInterface)); + } + + + /** + * Returns the {@link EntityInformation} for the given domain class. + * + * @param the entity type + * @param the id type + * @param domainClass + * @return + */ + public abstract EntityInformation getEntityInformation(Class domainClass); /** diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java index 4a189163f..ec236563b 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.support; +import java.io.Serializable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.ListableBeanFactory; @@ -32,8 +34,8 @@ import org.springframework.util.Assert; * * @author Oliver Gierke */ -public abstract class TransactionalRepositoryFactoryBeanSupport> - extends RepositoryFactoryBeanSupport implements BeanFactoryAware { +public abstract class TransactionalRepositoryFactoryBeanSupport, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport implements BeanFactoryAware { private String transactionManagerName = TxUtils.DEFAULT_TRANSACTION_MANAGER; private RepositoryProxyPostProcessor txPostProcessor; diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java index 9b114f0b2..aaac8c846 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java @@ -18,6 +18,8 @@ package org.springframework.data.repository.support; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import java.io.Serializable; + import org.junit.Test; @@ -38,14 +40,14 @@ public class AbstractEntityInformationUnitTests { @Test public void considersEntityNewIfGetIdReturnsNull() throws Exception { - EntityInformation metadata = + EntityInformation metadata = new DummyAbstractEntityInformation(Object.class); assertThat(metadata.isNew(null), is(true)); assertThat(metadata.isNew(new Object()), is(false)); } private static class DummyAbstractEntityInformation extends - AbstractEntityInformation { + AbstractEntityInformation { public DummyAbstractEntityInformation(Class domainClass) { @@ -60,9 +62,18 @@ public class AbstractEntityInformationUnitTests { * org.springframework.data.repository.support.EntityMetadata#getId( * java.lang.Object) */ - public Object getId(Object entity) { + public Serializable getId(Object entity) { return entity == null ? null : entity.toString(); } + + /* (non-Javadoc) + * @see org.springframework.data.repository.support.EntityInformation#getIdType() + */ + @Override + public Class getIdType() { + + return Serializable.class; + } } } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java index e14b917fd..0ced258e8 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java @@ -35,8 +35,8 @@ import org.springframework.data.domain.Persistable; public class PersistableEntityInformationUnitTests { @SuppressWarnings("rawtypes") - static final PersistableEntityInformation metadata = - new PersistableEntityInformation(Persistable.class); + static final PersistableEntityInformation metadata = + new PersistableEntityInformation(Persistable.class); @Mock Persistable persistable; @@ -64,8 +64,8 @@ public class PersistableEntityInformationUnitTests { @Test public void returnsGivenClassAsEntityType() throws Exception { - PersistableEntityInformation info = - new PersistableEntityInformation( + PersistableEntityInformation info = + new PersistableEntityInformation( PersistableEntity.class); assertEquals(PersistableEntity.class, info.getJavaType()); diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java index 8043fe941..875df7a76 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java @@ -62,6 +62,16 @@ public class RepositoryFactorySupportUnitTests { } class DummyRepositoryFactory extends RepositoryFactorySupport { + + /* (non-Javadoc) + * @see org.springframework.data.repository.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) + */ + @Override + public EntityInformation getEntityInformation( + Class domainClass) { + + return mock(EntityInformation.class); + } @Override protected Object getTargetRepository(RepositoryMetadata metadata) { diff --git a/spring-data-commons-extensions/pom.xml b/spring-data-commons-extensions/pom.xml new file mode 100644 index 000000000..44d281223 --- /dev/null +++ b/spring-data-commons-extensions/pom.xml @@ -0,0 +1,98 @@ + + 4.0.0 + + org.springframework.data + spring-data-commons-parent + 1.0.0.BUILD-SNAPSHOT + ../spring-data-commons-parent/pom.xml + + spring-data-commons-extensions + jar + Spring Data Commons Extensions + + + + ${project.groupId} + spring-data-commons-core + ${project.version} + + + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.springframework + spring-test + test + + + + + javax.servlet + servlet-api + 2.5 + provided + + + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + test + + + org.slf4j + slf4j-log4j12 + test + + + log4j + log4j + test + + + + javax.annotation + jsr250-api + true + + + + org.mockito + mockito-all + test + + + + junit + junit + + + + joda-time + joda-time + 1.6 + true + + + + + + + com.springsource.bundlor + com.springsource.bundlor.maven + + + + diff --git a/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditor.java b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditor.java new file mode 100644 index 000000000..505be98da --- /dev/null +++ b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditor.java @@ -0,0 +1,180 @@ +/* + * Copyright 2008-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.extensions.beans; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.io.Serializable; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + + +/** + * Generic {@link PropertyEditor} to map entities handled by a + * {@link Repository} to their id's and vice versa. + * + * @author Oliver Gierke + */ +public class DomainClassPropertyEditor extends + PropertyEditorSupport { + + private final Repository repository; + private final EntityInformation information; + private final PropertyEditorRegistry registry; + + + /** + * Creates a new {@link DomainClassPropertyEditor} for the given + * {@link Repository}. + * + * @param repository + * @param registry + */ + public DomainClassPropertyEditor(Repository repository, + EntityInformation information, + PropertyEditorRegistry registry) { + + Assert.notNull(repository); + Assert.notNull(registry); + + this.repository = repository; + this.information = information; + this.registry = registry; + } + + + /* + * (non-Javadoc) + * + * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String) + */ + @Override + public void setAsText(String idAsString) throws IllegalArgumentException { + + if (!StringUtils.hasText(idAsString)) { + setValue(null); + return; + } + + setValue(repository.findById(getId(idAsString))); + } + + + /* + * (non-Javadoc) + * + * @see java.beans.PropertyEditorSupport#getAsText() + */ + @Override + @SuppressWarnings("unchecked") + public String getAsText() { + + T entity = (T) getValue(); + + if (null == entity) { + return null; + } + + Object id = getId(entity); + return id == null ? null : id.toString(); + } + + + /** + * Looks up the id of the given entity using one of the + * {@link org.synyx.hades.dao.orm.GenericDaoSupport.IdAware} implementations + * of Hades. + * + * @param entity + * @return + */ + private ID getId(T entity) { + + return information.getId(entity); + } + + + /** + * Returns the actual typed id. Looks up an available customly registered + * {@link PropertyEditor} from the {@link PropertyEditorRegistry} before + * falling back on a {@link SimpleTypeConverter} to translate the + * {@link String} id into the type one. + * + * @param idAsString + * @return + */ + @SuppressWarnings("unchecked") + private ID getId(String idAsString) { + + Class idClass = information.getIdType(); + + PropertyEditor idEditor = registry.findCustomEditor(idClass, null); + + if (idEditor != null) { + idEditor.setAsText(idAsString); + return (ID) idEditor.getValue(); + } + + return new SimpleTypeConverter() + .convertIfNecessary(idAsString, idClass); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + DomainClassPropertyEditor that = + (DomainClassPropertyEditor) obj; + + return this.repository.equals(that.repository) + && this.registry.equals(that.registry) + && this.information.equals(that.information); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int hashCode = 17; + hashCode += repository.hashCode() * 32; + hashCode += information.hashCode() * 32; + hashCode += registry.hashCode() * 32; + return hashCode; + } +} diff --git a/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrar.java b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrar.java new file mode 100644 index 000000000..d5d09f1d0 --- /dev/null +++ b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrar.java @@ -0,0 +1,115 @@ +/* + * Copyright 2008-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.extensions.beans; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; +import org.springframework.data.repository.support.RepositoryFactoryInformation; + + +/** + * Simple helper class to use Hades DAOs to provide + * {@link java.beans.PropertyEditor}s for domain classes. To get this working + * configure a + * {@link org.springframework.web.bind.support.ConfigurableWebBindingInitializer} + * for your + * {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} + * and register the {@link DomainClassPropertyEditorRegistrar} there: + * <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> + * <property name="webBindingInitializer"> + * <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"> + * <property name="propertyEditorRegistrars"> + * <bean class="org.springframework.data.extensions.beans.DomainClassPropertyEditorRegistrar" /> + * </property> + * </bean> + * </property> + * </bean> + * Make sure this bean declaration is in the {@link ApplicationContext} + * created by the {@link DispatcherServlet} whereas the repositories need to be + * declared in the root + * {@link org.springframework.web.context.WebApplicationContext}. + * + * @author Oliver Gierke + */ +public class DomainClassPropertyEditorRegistrar implements + PropertyEditorRegistrar, ApplicationContextAware { + + private final Map, Repository> repositories = + new HashMap, Repository>(); + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.PropertyEditorRegistrar#registerCustomEditors + * (org.springframework.beans.PropertyEditorRegistry) + */ + public void registerCustomEditors(PropertyEditorRegistry registry) { + + for (Entry, Repository> entry : repositories + .entrySet()) { + + EntityInformation metadata = entry.getKey(); + Repository repository = entry.getValue(); + + DomainClassPropertyEditor editor = + new DomainClassPropertyEditor( + repository, metadata, registry); + + registry.registerCustomEditor(metadata.getJavaType(), editor); + } + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.context.ApplicationContextAware#setApplicationContext + * (org.springframework.context.ApplicationContext) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void setApplicationContext(ApplicationContext context) { + + Collection providers = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, + RepositoryFactoryInformation.class).values(); + + for (RepositoryFactoryInformation information : providers) { + + EntityInformation metadata = + information.getEntityInformation(); + Class> objectType = + information.getRepositoryInterface(); + Repository repository = + BeanFactoryUtils.beanOfType(context, objectType); + + this.repositories.put(metadata, repository); + } + } +} diff --git a/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/converter/DomainClassConverter.java b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/converter/DomainClassConverter.java new file mode 100644 index 000000000..a4df0b9fb --- /dev/null +++ b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/converter/DomainClassConverter.java @@ -0,0 +1,159 @@ +/* + * Copyright 2008-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.extensions.converter; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; +import org.springframework.data.repository.support.RepositoryFactoryInformation; + + +/** + * {@link org.springframework.core.convert.converter.Converter} to convert + * arbitrary input into domain classes managed by Spring Data {@link Repository} + * s. The implementation uses a {@link ConversionService} in turn to convert the + * source type into the domain class' id type which is then converted into a + * domain class object by using a {@link Repository}. + * + * @author Oliver Gierke + */ +public class DomainClassConverter implements ConditionalGenericConverter, + ApplicationContextAware { + + private final Map, Repository> repositories = + new HashMap, Repository>(); + private final ConversionService service; + + + /** + * Creates a new {@link DomainClassConverter}. + * + * @param service + */ + public DomainClassConverter(ConversionService service) { + + this.service = service; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.core.convert.converter.GenericConverter# + * getConvertibleTypes() + */ + public Set getConvertibleTypes() { + + return Collections.singleton(new ConvertiblePair(Object.class, + Object.class)); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.core.convert.converter.GenericConverter#convert(java + * .lang.Object, org.springframework.core.convert.TypeDescriptor, + * org.springframework.core.convert.TypeDescriptor) + */ + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + + EntityInformation info = + getRepositoryForDomainType(targetType.getType()); + + Repository repository = repositories.get(info); + Serializable id = service.convert(source, info.getIdType()); + return repository.findById(id); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.core.convert.converter.ConditionalGenericConverter + * #matches(org.springframework.core.convert.TypeDescriptor, + * org.springframework.core.convert.TypeDescriptor) + */ + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + + EntityInformation info = + getRepositoryForDomainType(targetType.getType()); + + if (info == null) { + return false; + } + + return service.canConvert(sourceType.getType(), info.getIdType()); + } + + + private EntityInformation getRepositoryForDomainType( + Class domainType) { + + for (EntityInformation information : repositories + .keySet()) { + + if (domainType.equals(information.getJavaType())) { + return information; + } + } + + return null; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.context.ApplicationContextAware#setApplicationContext + * (org.springframework.context.ApplicationContext) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void setApplicationContext(ApplicationContext context) { + + Collection providers = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, + RepositoryFactoryInformation.class).values(); + + for (RepositoryFactoryInformation entry : providers) { + + EntityInformation metadata = + entry.getEntityInformation(); + Class> objectType = + entry.getRepositoryInterface(); + Repository repository = + BeanFactoryUtils.beanOfType(context, objectType); + + this.repositories.put(metadata, repository); + } + } +} diff --git a/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableArgumentResolver.java b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableArgumentResolver.java new file mode 100644 index 000000000..92b04bcbf --- /dev/null +++ b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableArgumentResolver.java @@ -0,0 +1,327 @@ +/* + * Copyright 2008-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.extensions.web; + +import java.beans.PropertyEditorSupport; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletRequest; + +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.validation.DataBinder; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.ServletRequestParameterPropertyValues; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.context.request.NativeWebRequest; + + +/** + * Extracts paging information from web requests and thus allows injecting + * {@link Pageable} instances into controller methods. Request properties to be + * parsed can be configured. Default configuration uses request properties + * beginning with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}. + * + * @author Oliver Gierke + */ +public class PageableArgumentResolver implements WebArgumentResolver { + + private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10); + private static final String DEFAULT_PREFIX = "page"; + private static final String DEFAULT_SEPARATOR = "."; + + private Pageable fallbackPagable = DEFAULT_PAGE_REQUEST; + private String prefix = DEFAULT_PREFIX; + private String separator = DEFAULT_SEPARATOR; + + + /** + * Setter to configure a fallback instance of {@link Pageable} that is being + * used to back missing parameters. Defaults to + * {@value #DEFAULT_PAGE_REQUEST}. + * + * @param fallbackPagable the fallbackPagable to set + */ + public void setFallbackPagable(Pageable fallbackPagable) { + + this.fallbackPagable = + null == fallbackPagable ? DEFAULT_PAGE_REQUEST + : fallbackPagable; + } + + + /** + * Setter to configure the prefix of request parameters to be used to + * retrieve paging information. Defaults to {@link #DEFAULT_PREFIX}. + * + * @param prefix the prefix to set + */ + public void setPrefix(String prefix) { + + this.prefix = null == prefix ? DEFAULT_PREFIX : prefix; + } + + + /** + * Setter to configure the separator between prefix and actual property + * value. Defaults to {@link #DEFAULT_SEPARATOR}. + * + * @param separator the separator to set + */ + public void setSeparator(String separator) { + + this.separator = null == separator ? DEFAULT_SEPARATOR : separator; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.bind.support.WebArgumentResolver#resolveArgument + * (org.springframework.core.MethodParameter, + * org.springframework.web.context.request.NativeWebRequest) + */ + public Object resolveArgument(MethodParameter methodParameter, + NativeWebRequest webRequest) { + + if (methodParameter.getParameterType().equals(Pageable.class)) { + + assertPageableUniqueness(methodParameter); + + Pageable request = + getDefaultFromAnnotationOrFallback(methodParameter); + + ServletRequest servletRequest = + (ServletRequest) webRequest.getNativeRequest(); + + PropertyValues propertyValues = + new ServletRequestParameterPropertyValues(servletRequest, + getPrefix(methodParameter), separator); + + DataBinder binder = new ServletRequestDataBinder(request); + + binder.initDirectFieldAccess(); + binder.registerCustomEditor(Sort.class, new SortPropertyEditor( + "sort.dir", propertyValues)); + binder.bind(propertyValues); + + if (request.getPageNumber() > 0) { + + request = + new PageRequest(request.getPageNumber() - 1, + request.getPageSize(), request.getSort()); + } + + return request; + } + + return UNRESOLVED; + } + + + private Pageable getDefaultFromAnnotationOrFallback( + MethodParameter methodParameter) { + + // search for PageableDefaults annotation + for (Annotation annotation : methodParameter.getParameterAnnotations()) { + if (annotation instanceof PageableDefaults) { + PageableDefaults defaults = (PageableDefaults) annotation; + // +1 is because we substract 1 later + return new PageRequest(defaults.pageNumber() + 1, + defaults.value()); + } + } + + // Construct request with fallback request to ensure sensible + // default values. Create fresh copy as Spring will manipulate the + // instance under the covers + return new PageRequest(fallbackPagable.getPageNumber(), + fallbackPagable.getPageSize(), fallbackPagable.getSort()); + } + + + /** + * Resolves the prefix to use to bind properties from. Will prepend a + * possible {@link Qualifier} if available or return the configured prefix + * otherwise. + * + * @param parameter + * @return + */ + private String getPrefix(MethodParameter parameter) { + + for (Annotation annotation : parameter.getParameterAnnotations()) { + if (annotation instanceof Qualifier) { + return new StringBuilder(((Qualifier) annotation).value()) + .append("_").append(prefix).toString(); + } + } + + return prefix; + } + + + /** + * Asserts uniqueness of all {@link Pageable} parameters of the method of + * the given {@link MethodParameter}. + * + * @param parameter + */ + private void assertPageableUniqueness(MethodParameter parameter) { + + Method method = parameter.getMethod(); + + if (containsMoreThanOnePageableParameter(method)) { + Annotation[][] annotations = method.getParameterAnnotations(); + assertQualifiersFor(method.getParameterTypes(), annotations); + } + } + + + /** + * Returns whether the given {@link Method} has more than one + * {@link Pageable} parameter. + * + * @param method + * @return + */ + private boolean containsMoreThanOnePageableParameter(Method method) { + + boolean pageableFound = false; + + for (Class type : method.getParameterTypes()) { + + if (pageableFound && type.equals(Pageable.class)) { + return true; + } + + if (type.equals(Pageable.class)) { + pageableFound = true; + } + } + + return false; + } + + + /** + * Asserts that every {@link Pageable} parameter of the given parameters + * carries an {@link Qualifier} annotation to distinguish them from each + * other. + * + * @param parameterTypes + * @param annotations + */ + private void assertQualifiersFor(Class[] parameterTypes, + Annotation[][] annotations) { + + Set values = new HashSet(); + + for (int i = 0; i < annotations.length; i++) { + + if (Pageable.class.equals(parameterTypes[i])) { + + Qualifier qualifier = findAnnotation(annotations[i]); + + if (null == qualifier) { + throw new IllegalStateException( + "Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier"); + } + + if (values.contains(qualifier.value())) { + throw new IllegalStateException( + "Values of the user Qualifiers must be unique!"); + } + + values.add(qualifier.value()); + } + } + } + + + /** + * Returns a {@link Qualifier} annotation from the given array of + * {@link Annotation}s. Returns {@literal null} if the array does not + * contain a {@link Qualifier} annotation. + * + * @param annotations + * @return + */ + private Qualifier findAnnotation(Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation instanceof Qualifier) { + return (Qualifier) annotation; + } + } + + return null; + } + + /** + * {@link java.beans.PropertyEditor} to create {@link Sort} instances from + * textual representations. The implementation interprets the string as a + * comma separated list where the first entry is the sort direction ( + * {@code asc}, {@code desc}) followed by the properties to sort by. + * + * @author Oliver Gierke + */ + private static class SortPropertyEditor extends PropertyEditorSupport { + + private final String orderProperty; + private final PropertyValues values; + + + /** + * Creates a new {@link SortPropertyEditor}. + * + * @param orderProperty + * @param values + */ + public SortPropertyEditor(String orderProperty, PropertyValues values) { + + this.orderProperty = orderProperty; + this.values = values; + } + + + /* + * (non-Javadoc) + * + * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String) + */ + @Override + public void setAsText(String text) throws IllegalArgumentException { + + PropertyValue rawOrder = values.getPropertyValue(orderProperty); + Direction order = + null == rawOrder ? Direction.ASC : Direction + .fromString(rawOrder.getValue().toString()); + + setValue(new Sort(order, text)); + } + } +} diff --git a/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableDefaults.java b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableDefaults.java new file mode 100644 index 000000000..e59ce51bc --- /dev/null +++ b/spring-data-commons-extensions/src/main/java/org/springframework/data/extensions/web/PageableDefaults.java @@ -0,0 +1,50 @@ +/* + * Copyright 2008-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.extensions.web; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.domain.Pageable; + + +/** + * Annotation to set defaults when injecting a {@link Pageable} into a + * controller method. + * + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface PageableDefaults { + + /** + * The default-size the injected + * {@link org.springframework.data.domain.Pageable} should get if no + * corresponding parameter defined in request (default is 10). + */ + int value() default 10; + + + /** + * The default-pagenumber the injected + * {@link org.synyx.hades.domain.Pageable} should get if no corresponding + * parameter defined in request (default is 0). + */ + int pageNumber() default 0; +} diff --git a/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrarUnitTests.java b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrarUnitTests.java new file mode 100644 index 000000000..3fa638901 --- /dev/null +++ b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorRegistrarUnitTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2008-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.extensions.beans; + +import static org.hamcrest.Matchers.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; +import org.springframework.data.repository.support.RepositoryFactoryInformation; + + +/** + * Unit test for {@link DomainClassPropertyEditorRegistrar}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class DomainClassPropertyEditorRegistrarUnitTests { + + DomainClassPropertyEditorRegistrar registrar = + new DomainClassPropertyEditorRegistrar(); + @Mock + ApplicationContext context; + @Mock + PropertyEditorRegistry registry; + @Mock + EntityRepository repository; + @Mock + EntityInformation information; + @Mock + RepositoryFactoryInformation provider; + + DomainClassPropertyEditor reference; + + + @Before + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void setup() { + + when(information.getJavaType()).thenReturn(Entity.class); + when(provider.getEntityInformation()).thenReturn(information); + when(provider.getRepositoryInterface()).thenReturn( + (Class) EntityRepository.class); + Map map = getBeanAsMap(repository); + when(context.getBeansOfType(EntityRepository.class)).thenReturn(map); + + reference = + new DomainClassPropertyEditor(repository, + information, registry); + } + + + @Test + public void addsRepositoryForEntityIfAvailableInAppContext() + throws Exception { + + letContextContain(provider); + registrar.setApplicationContext(context); + registrar.registerCustomEditors(registry); + + verify(registry).registerCustomEditor(eq(Entity.class), eq(reference)); + } + + + @Test + public void doesNotAddDaoAtAllIfNoDaosFound() throws Exception { + + letContextContain(provider); + registrar.registerCustomEditors(registry); + + verify(registry, never()).registerCustomEditor(eq(Entity.class), + eq(reference)); + } + + + private void letContextContain(Object bean) { + + Map beanMap = getBeanAsMap(bean); + + when(context.getBeansOfType(argThat(is(subtypeOf(bean.getClass()))))) + .thenReturn(beanMap); + } + + + private Map getBeanAsMap(T bean) { + + Map beanMap = new HashMap(); + beanMap.put(bean.getClass().getName(), bean); + return beanMap; + } + + @SuppressWarnings("serial") + private static class Entity implements Serializable { + + } + + private static interface EntityRepository extends Repository { + + } + + + private static TypeSafeMatcher> subtypeOf( + final Class type) { + + return new TypeSafeMatcher>() { + + public void describeTo(Description arg0) { + + arg0.appendText("not a subtype of"); + } + + + @Override + public boolean matchesSafely(Class arg0) { + + return arg0.isAssignableFrom(type); + } + }; + } +} diff --git a/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorUnitTests.java b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorUnitTests.java new file mode 100644 index 000000000..4b7d93b40 --- /dev/null +++ b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/beans/DomainClassPropertyEditorUnitTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2008-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.extensions.beans; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.beans.PropertyEditor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.data.domain.Persistable; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; + + +/** + * Unit test for {@link DomainClassPropertyEditor}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class DomainClassPropertyEditorUnitTests { + + DomainClassPropertyEditor editor; + + @Mock + PropertyEditorRegistry registry; + @Mock + UserRepository userRepository; + @Mock + EntityInformation information; + + + @Before + public void setUp() { + + when(information.getIdType()).thenReturn(Integer.class); + editor = + new DomainClassPropertyEditor(userRepository, + information, registry); + } + + + @Test + public void convertsPlainIdTypeCorrectly() throws Exception { + + User user = new User(1); + when(information.getId(user)).thenReturn(user.getId()); + when(userRepository.findById(1)).thenReturn(user); + + editor.setAsText("1"); + + verify(userRepository, times(1)).findById(1); + } + + + @Test + public void convertsEntityToIdCorrectly() throws Exception { + + User user = new User(1); + editor.setValue(user); + when(information.getId(user)).thenReturn(user.getId()); + assertThat(editor.getAsText(), is("1")); + } + + + @Test + public void usesCustomEditorIfConfigured() throws Exception { + + PropertyEditor customEditor = mock(PropertyEditor.class); + when(customEditor.getValue()).thenReturn(1); + + when(registry.findCustomEditor(Integer.class, null)).thenReturn( + customEditor); + + convertsPlainIdTypeCorrectly(); + + verify(customEditor, times(1)).setAsText("1"); + } + + + @Test + public void returnsNullIdIfNoEntitySet() throws Exception { + + editor.setValue(null); + assertThat(editor.getAsText(), is(nullValue())); + } + + + @Test + public void resetsValueToNullAfterEmptyStringConversion() throws Exception { + + assertValueResetToNullAfterConverting(""); + } + + + @Test + public void resetsValueToNullAfterNullStringConversion() throws Exception { + + assertValueResetToNullAfterConverting(null); + } + + + private void assertValueResetToNullAfterConverting(String source) + throws Exception { + + convertsPlainIdTypeCorrectly(); + assertThat(editor.getValue(), is(notNullValue())); + + editor.setAsText(source); + assertThat(editor.getValue(), is(nullValue())); + } + + /** + * Sample entity. + * + * @author Oliver Gierke + */ + @SuppressWarnings("serial") + private static class User implements Persistable { + + private Integer id; + + + public User(Integer id) { + + this.id = id; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Persistable#getId() + */ + @Override + public Integer getId() { + + return id; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.domain.Persistable#isNew() + */ + @Override + public boolean isNew() { + + return getId() != null; + } + } + + /** + * Sample generic DAO interface. + * + * @author Oliver Gierke + */ + private static interface UserRepository extends Repository { + + } +} diff --git a/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/conversion/DomainClassConverterUnitTests.java b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/conversion/DomainClassConverterUnitTests.java new file mode 100644 index 000000000..5bb29a4c3 --- /dev/null +++ b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/conversion/DomainClassConverterUnitTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2008-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.extensions.conversion; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.extensions.converter.DomainClassConverter; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.support.EntityInformation; +import org.springframework.data.repository.support.RepositoryFactoryInformation; + + +/** + * Unit test for {@link DomainClassConverter}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class DomainClassConverterUnitTests { + + static final User USER = new User(); + + DomainClassConverter converter; + + TypeDescriptor sourceDescriptor; + TypeDescriptor targetDescriptor; + + Map providers; + + @Mock + ApplicationContext context; + @Mock + UserRepository repository; + @Mock + ConversionService service; + @Mock + EntityInformation information; + @Mock + RepositoryFactoryInformation provider; + + + @Before + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void setUp() { + + converter = new DomainClassConverter(service); + providers = new HashMap(); + + sourceDescriptor = TypeDescriptor.valueOf(String.class); + targetDescriptor = TypeDescriptor.valueOf(User.class); + + Map map = getBeanAsMap(repository); + when(context.getBeansOfType(UserRepository.class)).thenReturn(map); + when(context.getBeansOfType(RepositoryFactoryInformation.class)) + .thenReturn(providers); + when(provider.getEntityInformation()).thenReturn(information); + when(provider.getRepositoryInterface()).thenReturn( + (Class) UserRepository.class); + when(information.getJavaType()).thenReturn(User.class); + when(information.getIdType()).thenReturn(Long.class); + } + + + @Test + public void matchFailsIfNoDaoAvailable() throws Exception { + + converter.setApplicationContext(context); + assertMatches(false); + } + + + @Test + public void matchesIfConversionInBetweenIsPossible() throws Exception { + + letContextContain(provider); + converter.setApplicationContext(context); + + when(service.canConvert(String.class, Long.class)).thenReturn(true); + + assertMatches(true); + } + + + @Test + public void matchFailsIfNoIntermediateConversionIsPossible() + throws Exception { + + letContextContain(provider); + converter.setApplicationContext(context); + + when(service.canConvert(String.class, Long.class)).thenReturn(false); + + assertMatches(false); + } + + + private void assertMatches(boolean matchExpected) { + + assertThat(converter.matches(sourceDescriptor, targetDescriptor), + is(matchExpected)); + } + + + @Test + public void convertsStringToUserCorrectly() throws Exception { + + letContextContain(provider); + converter.setApplicationContext(context); + + when(service.canConvert(String.class, Long.class)).thenReturn(true); + when(service.convert(anyString(), eq(Long.class))).thenReturn(1L); + when(repository.findById(1L)).thenReturn(USER); + + Object user = + converter.convert("1", sourceDescriptor, targetDescriptor); + assertThat(user, is(instanceOf(User.class))); + assertThat(user, is((Object) USER)); + } + + + private void letContextContain(Object bean) { + + Map beanMap = getBeanAsMap(bean); + when(context.getBeansOfType(argThat(is(subtypeOf(bean.getClass()))))) + .thenReturn(beanMap); + } + + + private Map getBeanAsMap(T bean) { + + Map beanMap = new HashMap(); + beanMap.put(bean.getClass().getName(), bean); + return beanMap; + } + + + private static TypeSafeMatcher> subtypeOf( + final Class type) { + + return new TypeSafeMatcher>() { + + public void describeTo(Description arg0) { + + arg0.appendText("not a subtype of"); + } + + + @Override + public boolean matchesSafely(Class arg0) { + + return arg0.isAssignableFrom(type); + } + }; + } + + private static class User { + + } + + private static interface UserRepository extends Repository { + + } +} diff --git a/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/web/PageableArgumentResolverUnitTests.java b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/web/PageableArgumentResolverUnitTests.java new file mode 100644 index 000000000..4124801ec --- /dev/null +++ b/spring-data-commons-extensions/src/test/java/org/springframework/data/extensions/web/PageableArgumentResolverUnitTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2008-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.extensions.web; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + + +/** + * Unit test for {@link PageableArgumentResolver}. + * + * @author Oliver Gierke - gierke@synyx.de + */ +public class PageableArgumentResolverUnitTests { + + Method correctMethod; + Method failedMethod; + Method invalidQualifiers; + Method defaultsMethod; + + MockHttpServletRequest request; + + + @Before + public void setUp() throws SecurityException, NoSuchMethodException { + + correctMethod = + SampleController.class.getMethod("correctMethod", + Pageable.class, Pageable.class); + failedMethod = + SampleController.class.getMethod("failedMethod", + Pageable.class, Pageable.class); + invalidQualifiers = + SampleController.class.getMethod("invalidQualifiers", + Pageable.class, Pageable.class); + + defaultsMethod = + SampleController.class.getMethod("defaultsMethod", + Pageable.class); + + request = new MockHttpServletRequest(); + + // Add pagination info for foo table + request.addParameter("foo_page.size", "50"); + request.addParameter("foo_page.sort", "foo"); + request.addParameter("foo_page.sort.dir", "asc"); + + // Add pagination info for bar table + request.addParameter("bar_page.size", "60"); + } + + + @Test + public void testname() throws Exception { + + assertSizeForPrefix(50, new Sort(Direction.ASC, "foo"), 0); + assertSizeForPrefix(60, null, 1); + } + + + @Test(expected = IllegalStateException.class) + public void rejectsInvalidlyMappedPageables() throws Exception { + + MethodParameter parameter = new MethodParameter(failedMethod, 0); + NativeWebRequest webRequest = new ServletWebRequest(request); + + new PageableArgumentResolver().resolveArgument(parameter, webRequest); + } + + + @Test(expected = IllegalStateException.class) + public void rejectsInvalidQualifiers() throws Exception { + + MethodParameter parameter = new MethodParameter(invalidQualifiers, 0); + NativeWebRequest webRequest = new ServletWebRequest(request); + + new PageableArgumentResolver().resolveArgument(parameter, webRequest); + } + + + @Test + public void assertDefaults() throws Exception { + + MethodParameter parameter = new MethodParameter(defaultsMethod, 0); + NativeWebRequest webRequest = + new ServletWebRequest(new MockHttpServletRequest()); + PageableArgumentResolver resolver = new PageableArgumentResolver(); + Object argument = resolver.resolveArgument(parameter, webRequest); + + assertTrue(argument instanceof Pageable); + + Pageable pageable = (Pageable) argument; + assertEquals(SampleController.DEFAULT_PAGESIZE, pageable.getPageSize()); + assertEquals(SampleController.DEFAULT_PAGENUMBER, + pageable.getPageNumber()); + } + + + @Test + public void assertOverridesDefaults() throws Exception { + + Integer sizeParam = 5; + + MethodParameter parameter = new MethodParameter(defaultsMethod, 0); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + + mockRequest.addParameter("page.page", sizeParam.toString()); + NativeWebRequest webRequest = new ServletWebRequest(mockRequest); + PageableArgumentResolver resolver = new PageableArgumentResolver(); + Object argument = resolver.resolveArgument(parameter, webRequest); + + assertTrue(argument instanceof Pageable); + + Pageable pageable = (Pageable) argument; + assertEquals(SampleController.DEFAULT_PAGESIZE, pageable.getPageSize()); + assertEquals(sizeParam - 1, pageable.getPageNumber()); + } + + + private void assertSizeForPrefix(int size, Sort sort, int index) + throws Exception { + + MethodParameter parameter = new MethodParameter(correctMethod, index); + NativeWebRequest webRequest = new ServletWebRequest(request); + + PageableArgumentResolver resolver = new PageableArgumentResolver(); + + Object argument = resolver.resolveArgument(parameter, webRequest); + assertTrue(argument instanceof Pageable); + + Pageable pageable = (Pageable) argument; + assertEquals(size, pageable.getPageSize()); + + if (null != sort) { + assertEquals(sort, pageable.getSort()); + } + } + + @SuppressWarnings("unused") + private class SampleController { + + static final int DEFAULT_PAGESIZE = 198; + static final int DEFAULT_PAGENUMBER = 42; + + + public void defaultsMethod( + @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER) Pageable pageable) { + + } + + + public void correctMethod(@Qualifier("foo") Pageable first, + @Qualifier("bar") Pageable second) { + + } + + + public void failedMethod(Pageable first, Pageable second) { + + } + + + public void invalidQualifiers(@Qualifier("foo") Pageable first, + @Qualifier("foo") Pageable second) { + + } + } +} diff --git a/spring-data-commons-extensions/template.mf b/spring-data-commons-extensions/template.mf new file mode 100644 index 000000000..9979faab1 --- /dev/null +++ b/spring-data-commons-extensions/template.mf @@ -0,0 +1,16 @@ +Bundle-SymbolicName: org.springframework.data.extensions +Bundle-Name: Spring Data Commons Extensions +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Export-Template: + org.springframework.data.extensions.*;version="${project.version:[=.=.=,+1.0.0)}" +Import-Template: + org.springframework.data.*;version="${project.version:[=.=.=,+1.0.0)}", + org.springframework.core.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + org.springframework.beans.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + org.springframework.context.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + org.springframework.util.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + org.springframework.validation.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + org.springframework.web.*;version="${org.springframework.version:[=.=.=,+1.0.0)}", + javax.servlet.*;version="[2.5.0, 3.0.0)" + \ No newline at end of file diff --git a/spring-data-commons-parent/pom.xml b/spring-data-commons-parent/pom.xml index d36be31c8..71851a2b4 100644 --- a/spring-data-commons-parent/pom.xml +++ b/spring-data-commons-parent/pom.xml @@ -111,6 +111,16 @@ spring-tx ${org.springframework.version} + + org.springframework + spring-context + ${org.springframework.version} + + + org.springframework + spring-web + ${org.springframework.version} + org.springframework spring-test