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.
This commit is contained in:
Oliver Gierke
2011-03-11 15:59:15 +01:00
parent dd6e56d013
commit 80359c4d07
24 changed files with 1829 additions and 22 deletions

View File

@@ -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<T, ID extends Serializable> extends
PropertyEditorSupport {
private final Repository<T, ID> repository;
private final EntityInformation<T, ID> information;
private final PropertyEditorRegistry registry;
/**
* Creates a new {@link DomainClassPropertyEditor} for the given
* {@link Repository}.
*
* @param repository
* @param registry
*/
public DomainClassPropertyEditor(Repository<T, ID> repository,
EntityInformation<T, ID> 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<ID> 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;
}
}

View File

@@ -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: <code>
* &lt;bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"&gt;
* &lt;property name="webBindingInitializer"&gt;
* &lt;bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"&gt;
* &lt;property name="propertyEditorRegistrars"&gt;
* &lt;bean class="org.springframework.data.extensions.beans.DomainClassPropertyEditorRegistrar" /&gt;
* &lt;/property&gt;
* &lt;/bean&gt;
* &lt;/property&gt;
* &lt;/bean&gt;
* </code> 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<EntityInformation<Object, Serializable>, Repository<Object, Serializable>> repositories =
new HashMap<EntityInformation<Object, Serializable>, Repository<Object, Serializable>>();
/*
* (non-Javadoc)
*
* @see
* org.springframework.beans.PropertyEditorRegistrar#registerCustomEditors
* (org.springframework.beans.PropertyEditorRegistry)
*/
public void registerCustomEditors(PropertyEditorRegistry registry) {
for (Entry<EntityInformation<Object, Serializable>, Repository<Object, Serializable>> entry : repositories
.entrySet()) {
EntityInformation<Object, Serializable> metadata = entry.getKey();
Repository<Object, Serializable> repository = entry.getValue();
DomainClassPropertyEditor<Object, Serializable> editor =
new DomainClassPropertyEditor<Object, Serializable>(
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<RepositoryFactoryInformation> providers =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
RepositoryFactoryInformation.class).values();
for (RepositoryFactoryInformation information : providers) {
EntityInformation<Object, Serializable> metadata =
information.getEntityInformation();
Class<Repository<Object, Serializable>> objectType =
information.getRepositoryInterface();
Repository<Object, Serializable> repository =
BeanFactoryUtils.beanOfType(context, objectType);
this.repositories.put(metadata, repository);
}
}
}

View File

@@ -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<EntityInformation<?, Serializable>, Repository<?, Serializable>> repositories =
new HashMap<EntityInformation<?, Serializable>, Repository<?, Serializable>>();
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<ConvertiblePair> 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<?, Serializable> info =
getRepositoryForDomainType(targetType.getType());
Repository<?, Serializable> 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<?, Serializable> getRepositoryForDomainType(
Class<?> domainType) {
for (EntityInformation<?, Serializable> 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<RepositoryFactoryInformation> providers =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
RepositoryFactoryInformation.class).values();
for (RepositoryFactoryInformation entry : providers) {
EntityInformation<Object, Serializable> metadata =
entry.getEntityInformation();
Class<Repository<Object, Serializable>> objectType =
entry.getRepositoryInterface();
Repository<Object, Serializable> repository =
BeanFactoryUtils.beanOfType(context, objectType);
this.repositories.put(metadata, repository);
}
}
}

View File

@@ -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<String> values = new HashSet<String>();
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));
}
}
}

View File

@@ -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;
}

View File

@@ -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<Entity, Long> information;
@Mock
RepositoryFactoryInformation<Entity, Long> provider;
DomainClassPropertyEditor<Entity, Long> 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<String, EntityRepository> map = getBeanAsMap(repository);
when(context.getBeansOfType(EntityRepository.class)).thenReturn(map);
reference =
new DomainClassPropertyEditor<Entity, Long>(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<String, Object> beanMap = getBeanAsMap(bean);
when(context.getBeansOfType(argThat(is(subtypeOf(bean.getClass())))))
.thenReturn(beanMap);
}
private <T> Map<String, T> getBeanAsMap(T bean) {
Map<String, T> beanMap = new HashMap<String, T>();
beanMap.put(bean.getClass().getName(), bean);
return beanMap;
}
@SuppressWarnings("serial")
private static class Entity implements Serializable {
}
private static interface EntityRepository extends Repository<Entity, Long> {
}
private static <T> TypeSafeMatcher<Class<T>> subtypeOf(
final Class<? extends T> type) {
return new TypeSafeMatcher<Class<T>>() {
public void describeTo(Description arg0) {
arg0.appendText("not a subtype of");
}
@Override
public boolean matchesSafely(Class<T> arg0) {
return arg0.isAssignableFrom(type);
}
};
}
}

View File

@@ -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<User, Integer> editor;
@Mock
PropertyEditorRegistry registry;
@Mock
UserRepository userRepository;
@Mock
EntityInformation<User, Integer> information;
@Before
public void setUp() {
when(information.getIdType()).thenReturn(Integer.class);
editor =
new DomainClassPropertyEditor<User, Integer>(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<Integer> {
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<User, Integer> {
}
}

View File

@@ -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<String, RepositoryFactoryInformation> providers;
@Mock
ApplicationContext context;
@Mock
UserRepository repository;
@Mock
ConversionService service;
@Mock
EntityInformation<User, Long> information;
@Mock
RepositoryFactoryInformation<User, Long> provider;
@Before
@SuppressWarnings({ "unchecked", "rawtypes" })
public void setUp() {
converter = new DomainClassConverter(service);
providers = new HashMap<String, RepositoryFactoryInformation>();
sourceDescriptor = TypeDescriptor.valueOf(String.class);
targetDescriptor = TypeDescriptor.valueOf(User.class);
Map<String, UserRepository> 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<String, Object> beanMap = getBeanAsMap(bean);
when(context.getBeansOfType(argThat(is(subtypeOf(bean.getClass())))))
.thenReturn(beanMap);
}
private <T> Map<String, T> getBeanAsMap(T bean) {
Map<String, T> beanMap = new HashMap<String, T>();
beanMap.put(bean.getClass().getName(), bean);
return beanMap;
}
private static <T> TypeSafeMatcher<Class<T>> subtypeOf(
final Class<? extends T> type) {
return new TypeSafeMatcher<Class<T>>() {
public void describeTo(Description arg0) {
arg0.appendText("not a subtype of");
}
@Override
public boolean matchesSafely(Class<T> arg0) {
return arg0.isAssignableFrom(type);
}
};
}
private static class User {
}
private static interface UserRepository extends Repository<User, Long> {
}
}

View File

@@ -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) {
}
}
}