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