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:
@@ -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>();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Core components used across Spring Data REST.
|
||||
*/
|
||||
package org.springframework.data.rest.core;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}};
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user