Incorporated recent changes to Spring Data Commons and dependent projects that obsoleted the need to manage domain object metadata within Spring Data REST. Required updating to the latest snapshots available for spring-data-commons and spring-data-jpa.

Additional changes include:

* Re-wrote the monolithic Controller into separate controller classes that have a more narrow focus.
* Implemented common functionality as a `HandlerMethodArgumentResolver` rather than as a helper method in a controller class.
* Re-implemented JSONP functionality as an HttpMessageConverter rather than inline within a controller class.
* Updated to Jackson 2 for all JSON handling.
* By relying on spring-data-commons, spring-data-rest now handles all supported Repository types: JPA, MongoDB, and GemFire.

Added support for MongoDB and GemFire repositories by relying on spring-data-commons to provide the metadata rather than maintaining internal metadata information that is store-specific.

Replaced Spock spec tests with JMock unit and integration tests. Started integrating Jetty 8 into the testing so MVC testing can be done against a live server.
This commit is contained in:
Jon Brisbin
2013-01-04 11:13:04 -06:00
parent 269d6f5983
commit 6e4e7da142
242 changed files with 8466 additions and 9335 deletions

View File

@@ -1,4 +1,4 @@
package org.springframework.data.rest.core.convert;
package org.springframework.data.rest.convert;
import java.util.Stack;
@@ -7,14 +7,13 @@ import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
/**
* This {@link ConversionService} implementation delegates the actual conversion the ConversionService if finds in its
* internal List that claims to be able to convert a given class. It will roll through the internal <code>Stack</code>
* of <code>ConversionService</code>s until it finds one that can convert the given type.
* This {@link ConversionService} implementation delegates the actual conversion to the {@literal ConversionService} it
* finds in its internal {@link Stack} that claims to be able to convert a given class. It will roll through the
* {@literal ConversionService}s until it finds one that can convert the given type.
*
* @author Jon Brisbin
*/
public class DelegatingConversionService
implements ConversionService {
public class DelegatingConversionService implements ConversionService {
private Stack<ConversionService> conversionServices = new Stack<ConversionService>();

View File

@@ -0,0 +1,76 @@
package org.springframework.data.rest.convert;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
/**
* @author Jon Brisbin
*/
public class ISO8601DateConverter implements ConditionalGenericConverter,
Converter<String[], Date> {
public static final ConditionalGenericConverter INSTANCE = new ISO8601DateConverter();
private static final Set<ConvertiblePair> CONVERTIBLE_PAIRS = new HashSet<ConvertiblePair>();
static {
CONVERTIBLE_PAIRS.add(new ConvertiblePair(String.class, Date.class));
CONVERTIBLE_PAIRS.add(new ConvertiblePair(Date.class, String.class));
}
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if(String.class.isAssignableFrom(sourceType.getType())) {
return Date.class.isAssignableFrom(targetType.getType());
}
return Date.class.isAssignableFrom(sourceType.getType())
&& String.class.isAssignableFrom(targetType.getType());
}
@Override public Set<ConvertiblePair> getConvertibleTypes() {
return CONVERTIBLE_PAIRS;
}
@Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
DateFormat dateFmt = iso8601DateFormat();
if(String.class.isAssignableFrom(sourceType.getType())) {
return dateFmt.format(source);
} else {
try {
return dateFmt.parse(source.toString());
} catch(ParseException e) {
throw new ConversionFailedException(sourceType, targetType, source, e);
}
}
}
@Override public Date convert(String[] source) {
if(source.length > 0) {
try {
return iso8601DateFormat().parse(source[0]);
} catch(ParseException e) {
throw new ConversionFailedException(
TypeDescriptor.valueOf(String[].class),
TypeDescriptor.valueOf(Date.class),
source[0],
new IllegalArgumentException("Source does not conform to ISO8601 date format (YYYY-MM-DDTHH:MM:SS-0000")
);
}
}
return null;
}
private DateFormat iso8601DateFormat() {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
}
}

View File

@@ -0,0 +1,46 @@
package org.springframework.data.rest.convert;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
/**
* For converting a {@link UUID} into a {@link String}.
*
* @author Jon Brisbin
*/
public class UUIDConverter implements ConditionalGenericConverter {
public static final UUIDConverter INSTANCE = new UUIDConverter();
private static final Set<ConvertiblePair> CONVERTIBLE_PAIRS = new HashSet<ConvertiblePair>();
static {
CONVERTIBLE_PAIRS.add(new ConvertiblePair(String.class, UUID.class));
CONVERTIBLE_PAIRS.add(new ConvertiblePair(UUID.class, String.class));
}
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if(String.class.isAssignableFrom(sourceType.getType())) {
return UUID.class.isAssignableFrom(targetType.getType());
}
return UUID.class.isAssignableFrom(sourceType.getType())
&& String.class.isAssignableFrom(targetType.getType());
}
@Override public Set<ConvertiblePair> getConvertibleTypes() {
return CONVERTIBLE_PAIRS;
}
@Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if(String.class.isAssignableFrom(sourceType.getType())) {
return UUID.fromString(source.toString());
} else {
return source.toString();
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* {@link org.springframework.core.convert.ConversionService} and {@link org.springframework.core.convert.converter.Converter} integration for Spring Data REST.
*/
package org.springframework.data.rest.convert;

View File

@@ -1,20 +0,0 @@
package org.springframework.data.rest.core;
/**
* Generic interface used as a callback in any place you need extensibility.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface Handler<T, V> {
/**
* Accept an argument and possibly produce a result.
*
* @param t
* arg
*
* @return Some object or {@literal null} if no result.
*/
V handle(T t);
}

View File

@@ -1,24 +0,0 @@
package org.springframework.data.rest.core;
import java.net.URI;
/**
* Implementations of this interface are responsible for turning {@link URI}s into real objects.
*
* @author Jon Brisbin
*/
public interface UriResolver<T> {
/**
* Take a {@link URI} and resolve it to an actual object.
*
* @param baseUri
* The base URI that this resource is relative to.
* @param uri
* The URI id of the resource.
*
* @return The resolved object or {@literal null} if not found.
*/
T resolve(URI baseUri, URI uri);
}

View File

@@ -1,15 +0,0 @@
package org.springframework.data.rest.core.convert;
import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class StringToUUIDConverter
implements Converter<String, UUID> {
@Override public UUID convert(String s) {
return (null != s ? UUID.fromString(s) : null);
}
}

View File

@@ -1,15 +0,0 @@
package org.springframework.data.rest.core.convert;
import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class UUIDToStringConverter
implements Converter<UUID, String> {
@Override public String convert(UUID uuid) {
return (null != uuid ? uuid.toString() : null);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Core components used across Spring Data REST.
*/
package org.springframework.data.rest.core;

View File

@@ -1,213 +0,0 @@
package org.springframework.data.rest.core.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class BeanUtils {
private BeanUtils() {
}
public static ConfigurableConversionService CONVERSION_SERVICE = new DefaultConversionService();
private static final LoadingCache<Object[], Field> fields = CacheBuilder.newBuilder().build(
new CacheLoader<Object[], Field>() {
@Override public Field load(Object[] key)
throws Exception {
Class<?> clazz = (Class<?>)key[0];
String name = (String)key[1];
Field f = ReflectionUtils.findField(clazz, name);
if(null != f) {
ReflectionUtils.makeAccessible(f);
return f;
} else {
throw new IllegalArgumentException("Field " + clazz.getName() + "." + name + " not found");
}
}
}
);
private static final LoadingCache<Object[], Method> methods = CacheBuilder.newBuilder().build(
new CacheLoader<Object[], Method>() {
@Override public Method load(Object[] key)
throws Exception {
Class<?> clazz = (Class<?>)key[0];
String name = (String)key[1];
Integer paramCnt = key.length == 3 ? (Integer)key[2] : 0;
for(Method m : clazz.getDeclaredMethods()) {
if(m.getName().equals(name)) {
if(m.getParameterTypes().length == paramCnt) {
ReflectionUtils.makeAccessible(m);
return m;
}
}
}
throw new IllegalArgumentException("Method " + clazz.getName() + "." + name + " not found");
}
}
);
public static boolean hasProperty(String property, Object... objs) {
for(Object obj : objs) {
if(obj instanceof Map) {
return ((Map)obj).containsKey(property);
}
Class<?> type = obj.getClass();
try {
if(FluentBeanUtils.isFluentBean(type)) {
return null != methods.get(new Object[]{type, property});
} else {
if(null == methods.get(new Object[]{type, "get" + StringUtils.capitalize(property)})) {
return null != fields.get(new Object[]{type, property});
} else {
return true;
}
}
} catch(UncheckedExecutionException e) {
if(e.getCause().getClass() == IllegalArgumentException.class) {
return false;
} else {
throw new IllegalStateException(e);
}
} catch(ExecutionException e) {
throw new IllegalStateException(e);
}
}
return false;
}
@SuppressWarnings({"unchecked"})
public static <T> T findFirst(Class<T> clazz, List<?> stack) {
for(Object o : stack) {
if(ClassUtils.isAssignable(clazz, o.getClass())) {
return (T)o;
}
}
return null;
}
@SuppressWarnings({"unchecked"})
public static Object findFirst(Object o, Object... objs) {
for(Object obj : objs) {
if(o == obj || null != o && o.equals(obj)) {
return obj;
} else if(obj instanceof List) {
return Collections.binarySearch((List)obj, o);
} else if(obj instanceof Object[]) {
return Arrays.binarySearch((Object[])obj, o);
}
}
return null;
}
public static Object findFirst(String property, Object... objs) {
for(Object obj : objs) {
if(obj instanceof Map) {
return ((Map)obj).get(property);
}
Class<?> type = obj.getClass();
try {
Field f = fields.get(new Object[]{type, property});
if(FluentBeanUtils.isFluentBean(type)) {
return FluentBeanUtils.get(property, obj);
} else {
Method getter = methods.get(new Object[]{type, "get" + StringUtils.capitalize(property)});
try {
if(null != getter) {
return getter.invoke(obj);
} else {
return f.get(obj);
}
} catch(IllegalAccessException e) {
throw new IllegalStateException(e);
} catch(InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
} catch(IllegalArgumentException e) {
} catch(ExecutionException e) {
throw new IllegalArgumentException(e);
}
}
return null;
}
public static boolean containsType(Class<?> type, List<Object> objs) {
return containsType(type, objs.toArray());
}
public static boolean containsType(Class<?> type, Object[] objs) {
for(Object obj : objs) {
if(null != obj && ClassUtils.isAssignable(obj.getClass(), type)) {
return true;
}
}
return false;
}
@SuppressWarnings({"unchecked"})
public static Object invoke(String methodName, Object target, Object... args) {
return invoke(methodName, target, Object.class, args);
}
@SuppressWarnings({"unchecked"})
public static <T> T invoke(String methodName, Object target, Class<T> returnType, Object... args) {
if(null == target) {
return null;
}
Class<?> type = target.getClass();
try {
Method m = methods.get(new Object[]{type, methodName, args.length});
List<Object> newArgs = new ArrayList<Object>(args.length);
Class<?>[] paramTypes = m.getParameterTypes();
for(int i = 0; i < args.length; i++) {
Object o = args[i];
Class<?> oType = o.getClass();
Class<?> pType = paramTypes[i];
if(!ClassUtils.isAssignable(oType, pType)) {
newArgs.add(CONVERSION_SERVICE.convert(o, pType));
} else {
newArgs.add(o);
}
}
Object rtnVal = m.invoke(target, newArgs.toArray());
if((returnType != Void.TYPE || returnType != Object.class)
&& null != rtnVal
&& !ClassUtils.isAssignable(returnType, rtnVal.getClass())) {
return CONVERSION_SERVICE.convert(rtnVal, returnType);
} else {
return (T)rtnVal;
}
} catch(IllegalArgumentException e) {
} catch(Exception e) {
throw new IllegalStateException(e);
}
return null;
}
}

View File

@@ -1,189 +0,0 @@
package org.springframework.data.rest.core.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.springframework.util.ReflectionUtils;
/**
* Helper methods for dealing with the metadata of "fluent" beans.
*
* @author Jon Brisbin
*/
public abstract class FluentBeanUtils {
private static final LoadingCache<Class<?>, Metadata> metadata = CacheBuilder.newBuilder().build(
new CacheLoader<Class<?>, Metadata>() {
@Override public Metadata load(Class<?> type)
throws Exception {
final Metadata meta = new Metadata();
ReflectionUtils.doWithFields(
type,
new ReflectionUtils.FieldCallback() {
@Override public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
final String fname = field.getName();
if(!fname.startsWith("_")) {
ReflectionUtils.doWithMethods(field.getDeclaringClass(), new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method)
throws IllegalArgumentException, IllegalAccessException {
if(method.getName().equals(fname)) {
ReflectionUtils.makeAccessible(method);
if(method.getParameterTypes().length == 0) {
meta.getters.put(fname, method);
} else if(method.getParameterTypes().length == 1) {
meta.setters.put(fname, method);
}
meta.fieldNames.add(fname);
}
}
});
ReflectionUtils.makeAccessible(field);
meta.fields.put(fname, field);
}
}
}
);
return meta;
}
}
);
/**
* Interrogate a bean and collect {@link Metadata} on it.
*
* @param targetType
* The type to interrogate.
*
* @return {@link Metadata} for the fluent bean.
*/
public static Metadata metadata(Class<?> targetType) {
try {
return metadata.get(targetType);
} catch(ExecutionException e) {
throw new IllegalStateException(e);
}
}
/**
* Set the property of a fluent bean.
*
* @param property
* Name of the property to set.
* @param value
* Value of the property.
* @param bean
* Bean on which to set this property.
*
* @return Usually {@literal null} but will return whatever the "setter" returns, which could be {@this} or something
* else.
*/
public static Object set(String property, Object value, Object bean) {
if(null == bean) {
return null;
}
Class<?> type = bean.getClass();
try {
Method setter = metadata.get(type).setters.get(property);
if(null != setter) {
return setter.invoke(bean, value);
}
Field f = metadata.get(type).fields.get(property);
if(null == f) {
return null;
}
f.set(bean, value);
return bean;
} catch(Throwable t) {
throw new IllegalArgumentException(t.getMessage(), t);
}
}
/**
* Get the value of a property.
*
* @param property
* Name of the property.
* @param bean
* Bean of which to get the property.
*
* @return Value of the property. Could be {@literal null}
*/
public static Object get(String property, Object bean) {
if(null == bean) {
return null;
}
Class<?> type = bean.getClass();
try {
Method getter = metadata.get(type).getters.get(property);
if(null != getter) {
return getter.invoke(bean);
}
Field f = metadata.get(type).fields.get(property);
if(null == f) {
return null;
}
return f.get(bean);
} catch(Throwable t) {
throw new IllegalStateException(t.getMessage(), t);
}
}
/**
* Determines whether a given type looks like a fluent bean. That means it has methods whose names exactly correspond
* to a field of the same name. A "getter" is that method which is named the same as the field and has 0 parameters.
* The "setter" is that method which is named the same as the field and has a single argument.
*
* @param type
* The class to inspect.
*
* @return {@literal true} if this looks like a fluent bean, {@literal false} otherwise.
*/
public static boolean isFluentBean(Class<?> type) {
try {
return metadata.get(type).getters.size() > 0;
} catch(ExecutionException e) {
throw new IllegalStateException(e);
}
}
public static class Metadata {
List<String> fieldNames = new ArrayList<String>();
Map<String, Field> fields = new HashMap<String, Field>();
Map<String, Method> getters = new HashMap<String, Method>();
Map<String, Method> setters = new HashMap<String, Method>();
public List<String> fieldNames() {
return fieldNames;
}
public Map<String, Method> getters() {
return getters;
}
public Map<String, Method> setters() {
return setters;
}
public Map<String, Field> fields() {
return fields;
}
}
}

View File

@@ -1,43 +0,0 @@
package org.springframework.data.rest.core.util;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class RestHelper<T> {
public HttpStatus status;
public HttpHeaders headers = new HttpHeaders();
public T body;
private RestHelper(T body) {
this.body = body;
}
public static <T> RestHelper<T> resource(T body) {
return new RestHelper<T>(body);
}
public RestHelper<T> header(String key, String value) {
headers.add(key, value);
return this;
}
public RestHelper<T> status(HttpStatus status) {
this.status = status;
return this;
}
public HttpEntity<T> asHttpEntity() {
return new HttpEntity<T>(body, headers);
}
public ResponseEntity<T> asResponseEntity() {
return new ResponseEntity<T>(body, headers, status);
}
}

View File

@@ -4,7 +4,7 @@ import java.net.URI;
import java.util.List;
import java.util.Stack;
import org.springframework.data.rest.core.Handler;
import com.google.common.base.Function;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
@@ -37,7 +37,7 @@ public abstract class UriUtils {
}
/**
* Execute the given {@link Handler} for each segment in the {@link URI}.
* Execute the given {@link Function} for each segment in the {@link URI}.
* <p>e.g. given a URI of {@literal http://localhost:8080/data/person/1} and a base URI of {@code
* http://localhost:8080/data}, this method will explode the URI into it's components, as compared to the base URI.
* The result would be: the given handler gets called twice, once passing a relative {@link URI} of "person" and a
@@ -47,19 +47,19 @@ public abstract class UriUtils {
* @param baseUri
* base {@link URI}
* @param uri
* {@link URI} to explode and iteratre over.
* {@link URI} to explode and iterate over.
* @param handler
* {@link Handler} to call for each segment of the URI's path.
* {@link Function} to call for each segment of the URI's path.
* @param <V>
* Return type of the handler.
*
* @return Handler return value, or possibly {@literal null}.
*/
public static <V> V foreach(URI baseUri, URI uri, Handler<URI, V> handler) {
public static <V> V foreach(URI baseUri, URI uri, Function<URI, V> handler) {
List<URI> uris = explode(baseUri, uri);
V v = null;
for(URI u : uris) {
v = handler.handle(u);
v = handler.apply(u);
}
return v;
}
@@ -163,8 +163,9 @@ public abstract class UriUtils {
* Just the path portion of the {@link URI}, but with any trailing slash "/" removed.
*
* @param uri
* path URI
*
* @return
* @return the path portion of the URI, but with any trailing slash removed
*/
public static String path(URI uri) {
if(null == uri) {
@@ -197,9 +198,11 @@ public abstract class UriUtils {
* Create a new {@link URI} out of the components.
*
* @param baseUri
* The base URI these path segments are relative to.
* @param pathSegments
* The path segments to add to the given base URI.
*
* @return
* @return A new URI built from the given base URI and additional path segments.
*/
public static URI buildUri(URI baseUri, String... pathSegments) {
return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri();

View File

@@ -1,48 +0,0 @@
package org.springframework.data.rest.core.spec
import org.springframework.data.rest.core.util.UriUtils
import spock.lang.Specification
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
class UriUtilsSpec extends Specification {
def "merges URIs correctly"() {
given:
// (absolute) URI of the base resource
def baseUri = new URI("http://localhost:8080/baseUrl")
// (relative) URI of the top-level Resource
def uri2 = new URI("resource")
// (relative) URI of the second-level Resource
def uri3 = new URI("1")
// (fragment) URI of the bottom-level Resource
def uri4 = new URI("count")
when:
def uri5 = UriUtils.merge(baseUri, uri2, uri3, uri4)
then:
uri5.toString() == "http://localhost:8080/baseUrl/resource/1/count"
}
def "explodes URIs correctly"() {
given:
// (absolute) URI of the base resource
def baseUri = new URI("http://localhost:8080/baseUrl")
// (absolute) URI of the full resource to get a path to
def resourceUri = new URI("http://localhost:8080/baseUrl/resource/1/property")
when:
def uris = UriUtils.explode(baseUri, resourceUri)
then:
uris.size() == 3
uris[2].path == "property"
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.data.rest;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.runner.RunWith;
/**
* Abstract base classes for JUnit tests that use JMock.
*
* @author Jon Brisbin
*/
@RunWith(JMock.class)
public abstract class AbstractJMockTests {
protected JUnit4Mockery context = new JUnit4Mockery() {{
setImposteriser(ClassImposteriser.INSTANCE);
}};
}

View File

@@ -0,0 +1,64 @@
package org.springframework.data.rest.convert;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import java.util.UUID;
import org.jmock.Expectations;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.rest.AbstractJMockTests;
import org.springframework.format.support.DefaultFormattingConversionService;
/**
* Tests to ensure the {@link DelegatingConversionService} properly delegates conversions to the {@link
* org.springframework.core.convert.ConversionService} that is appropriate for the given source and return types.
*
* @author Jon Brisbin
*/
public class DelegatingConversionServiceUnitTests extends AbstractJMockTests {
private static final UUID RANDOM_UUID = UUID.fromString("9deccfd7-f892-4e26-a4d5-c92893392e78");
private ConversionService conversionService;
private DelegatingConversionService delegatingConversionService;
@Before
public void setup() {
conversionService = context.mock(ConversionService.class);
DefaultFormattingConversionService cs = new DefaultFormattingConversionService(false);
cs.addConverter(UUIDConverter.INSTANCE);
delegatingConversionService = new DelegatingConversionService(
conversionService,
cs
);
context.checking(new Expectations() {{
allowing(conversionService).canConvert(String.class, UUID.class);
will(returnValue(false));
allowing(conversionService).canConvert(UUID.class, String.class);
will(returnValue(false));
// Ensure the first ConversionService is never asked to convert this String into a UUID
never(conversionService).convert(with(any(String.class)), with(UUID.class));
never(conversionService).convert(with(any(UUID.class)), with(String.class));
}});
}
@Test
public void shouldDelegateToProperConversionService() throws Exception {
assertThat(delegatingConversionService.canConvert(String.class, UUID.class), is(true));
assertThat(delegatingConversionService.convert(RANDOM_UUID.toString(), UUID.class), is(RANDOM_UUID));
}
@Test
public void shouldConvertUUIDToString() throws Exception {
assertThat(delegatingConversionService.canConvert(UUID.class, String.class), is(true));
assertThat(delegatingConversionService.convert(RANDOM_UUID, String.class), is(RANDOM_UUID.toString()));
}
}

View File

@@ -0,0 +1,93 @@
package org.springframework.data.rest.core.util;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import com.google.common.base.Function;
import org.junit.Test;
/**
* Tests to verify that {@link UriUtils} can manipulate {@link URI}s.
*
* @author Jon Brisbin
*/
public class UriUtilsUnitTests {
private static final String BASE_URI_STR = "http://localhost:8080/data";
private static final URI BASE_URI = URI.create(BASE_URI_STR);
private static final String PERSON_2LVL_STR = BASE_URI_STR + "/person/1";
private static final URI PERSON_2LVL_URI = URI.create(PERSON_2LVL_STR);
@Test
public void shouldValidateBaseURI() throws Exception {
URI uri = new URI(BASE_URI + "/person/1");
assertThat(UriUtils.validBaseUri(BASE_URI, uri), is(true));
}
@Test
public void shouldIterateOverPathElements() throws Exception {
final List<String> paths = new ArrayList<String>();
Function<URI, Void> fn = new Function<URI, Void>() {
@Override public Void apply(URI uri) {
paths.add(uri.getPath());
return null;
}
};
UriUtils.foreach(BASE_URI, PERSON_2LVL_URI, fn);
assertThat(paths, hasSize(2));
assertThat(paths, contains("person", "1"));
}
@Test
public void shouldExplodeRelativeURI() throws Exception {
Stack<URI> uris = UriUtils.explode(BASE_URI, PERSON_2LVL_URI);
assertThat(uris, hasSize(2));
assertThat(uris, contains(URI.create("person"), URI.create("1")));
}
@Test
public void shouldMergeDifferentURIsIntoOne() throws Exception {
String qrystr = "?queryParam=testValue";
URI uriWithQuery = URI.create(qrystr);
URI uriWithPath = URI.create("person/1");
URI uri = UriUtils.merge(BASE_URI, uriWithPath, uriWithQuery);
assertThat(uri.toString(), is(PERSON_2LVL_STR + qrystr));
}
@Test
public void shouldStripTrailingSlashFromPath() throws Exception {
URI uri = URI.create("person/");
String path = UriUtils.path(uri);
assertThat(path, is("person"));
}
@Test
public void shouldStripTheLastPathSegmentFromAURI() throws Exception {
URI uri = UriUtils.tail(BASE_URI, PERSON_2LVL_URI);
assertThat(uri, is(URI.create("1")));
}
@Test
public void shouldBuildURIFromPathSegments() throws Exception {
URI uri = UriUtils.buildUri(BASE_URI, "person", "1");
assertThat(uri, is(PERSON_2LVL_URI));
}
}

View File

@@ -8,8 +8,7 @@
</encoder>
</appender>
<logger name="org.springframework.data.services" level="DEBUG"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.springframework.data.rest" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="stdout"/>