commit 3dc54953575b152fef74662dd57a9b362e8af332 Author: Jon Brisbin Date: Thu Mar 8 15:57:24 2012 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..23650c61f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.gradle/ +build/ +*.i* \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..959ad4e53 --- /dev/null +++ b/build.gradle @@ -0,0 +1,76 @@ +apply plugin: "base" + +allprojects { + apply plugin: "idea" + apply plugin: "maven" + + group = "org.springframework.data.rest" + version = "$sdRestVersion" + + releaseBuild = version.endsWith("RELEASE") + snapshotBuild = version.endsWith("SNAPSHOT") + + sourceCompatibility = 6 + targetCompatibility = 6 + + configurations.all { + exclude group: "commons-logging" + exclude module: "slf4j-log4j12" + exclude module: "groovy-all", version: "1.8.0-beta-3-SNAPSHOT" + } + +} + +subprojects { + + repositories { + mavenCentral(artifactUrls: [ + "http://maven.springframework.org/release", + "http://maven.springframework.org/milestone", + //"http://repository.jboss.org/maven2/", + //"http://download.java.net/maven/2/" + ]) + mavenLocal() + } + + apply plugin: "java" + apply plugin: "groovy" + + [compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:-serial", "-Xlint:-unchecked"] + + dependencies { + groovy "org.codehaus.groovy:groovy:$groovyVersion" + + // Logging + compile "org.slf4j:slf4j-api:$slf4jVersion" + runtime "org.slf4j:jcl-over-slf4j:$slf4jVersion" + runtime "ch.qos.logback:logback-classic:$logbackVersion" + + // Jackson JSON + compile "org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion" + + // Spring + compile "org.springframework:spring-beans:$springVersion" + compile "org.springframework:spring-context:$springVersion" + compile "org.springframework:spring-core:$springVersion" + compile "org.springframework:spring-web:$springVersion" + + // Testing + testCompile "org.spockframework:spock-core:$spockVersion" + testCompile "org.spockframework:spock-spring:$spockVersion" + testCompile "org.hamcrest:hamcrest-all:1.1" + testCompile "org.springframework:spring-test:$springVersion" + testRuntime "org.springframework:spring-context-support:$springVersion" + + } + +} + +task wrapper(type: Wrapper) { gradleVersion = "1.0-milestone-8" } + +idea { + project.ipr.withXml { provider -> + provider.node.component.find { it.@name == 'VcsDirectoryMappings' }.mapping.@vcs = 'Git' + } + module.jdkName = "OpenJDK 1.7" +} diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 000000000..1b01ab021 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,6 @@ +dependencies { + + // Google Guava + compile "com.google.guava:guava:11.0.1" + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/Handler.java b/core/src/main/java/org/springframework/data/rest/core/Handler.java new file mode 100644 index 000000000..e0466fab1 --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/Handler.java @@ -0,0 +1,8 @@ +package org.springframework.data.rest.core; + +/** + * @author Jon Brisbin + */ +public interface Handler { + V handle(T t); +} diff --git a/core/src/main/java/org/springframework/data/rest/core/Link.java b/core/src/main/java/org/springframework/data/rest/core/Link.java new file mode 100644 index 000000000..c22dd684c --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/Link.java @@ -0,0 +1,14 @@ +package org.springframework.data.rest.core; + +import java.net.URI; + +/** + * @author Jon Brisbin + */ +public interface Link { + + String rel(); + + URI href(); + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/SimpleLink.java b/core/src/main/java/org/springframework/data/rest/core/SimpleLink.java new file mode 100644 index 000000000..0baf9f3d3 --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/SimpleLink.java @@ -0,0 +1,33 @@ +package org.springframework.data.rest.core; + +import java.net.URI; + +/** + * @author Jon Brisbin + */ +public class SimpleLink implements Link { + + private String rel; + private URI href; + + public SimpleLink(String rel, URI href) { + this.rel = rel; + this.href = href; + } + + @Override public String rel() { + return rel; + } + + @Override public URI href() { + return href; + } + + @Override public String toString() { + return "SimpleLink{" + + "rel='" + rel + '\'' + + ", href=" + href + + '}'; + } + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java b/core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java new file mode 100644 index 000000000..547a0a40f --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java @@ -0,0 +1,211 @@ +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 + */ +public abstract class BeanUtils { + + private BeanUtils() { + } + + public static ConfigurableConversionService CONVERSION_SERVICE = new DefaultConversionService(); + + private static final LoadingCache fields = CacheBuilder.newBuilder().build( + new CacheLoader() { + @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 methods = CacheBuilder.newBuilder().build( + new CacheLoader() { + @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 findFirst(Class 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 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 invoke(String methodName, Object target, Class returnType, Object... args) { + if (null == target) { + return null; + } + + Class type = target.getClass(); + try { + Method m = methods.get(new Object[]{type, methodName, args.length}); + List newArgs = new ArrayList(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; + } + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java new file mode 100644 index 000000000..ac8083859 --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java @@ -0,0 +1,89 @@ +package org.springframework.data.rest.core.util; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.deser.std.StdDeserializer; +import org.springframework.core.convert.ConversionService; +import org.springframework.util.ClassUtils; + +/** + * @author Jon Brisbin + */ +public class FluentBeanDeserializer extends StdDeserializer { + + private ConversionService conversionService; + private FluentBeanUtils.Metadata beanMeta; + + @SuppressWarnings({"unchecked"}) + public FluentBeanDeserializer(final Class valueClass, ConversionService conversionService) { + super(valueClass); + this.conversionService = conversionService; + this.beanMeta = FluentBeanUtils.metadata(valueClass); + + if (!FluentBeanUtils.isFluentBean(valueClass)) { + throw new IllegalArgumentException("Class of type " + valueClass + " is not a FluentBean"); + } + } + + @Override + public Object deserialize(JsonParser jp, + DeserializationContext ctxt) + throws IOException, + JsonProcessingException { + if (jp.getCurrentToken() != JsonToken.START_OBJECT) { + throw ctxt.mappingException(_valueClass); + } + + Object bean; + try { + bean = _valueClass.newInstance(); + } catch (InstantiationException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + + while (jp.nextToken() != JsonToken.END_OBJECT) { + String name = jp.getCurrentName(); + Method setter = beanMeta.setters().get(name); + + Object obj; + if (null != setter) { + Class targetType = setter.getParameterTypes()[0]; + if (ClassUtils.isAssignable(targetType, Long.class)) { + obj = jp.nextLongValue(-1); + } else if (ClassUtils.isAssignable(targetType, Integer.class)) { + obj = jp.nextIntValue(-1); + } else if (ClassUtils.isAssignable(targetType, Boolean.class)) { + obj = jp.nextBooleanValue(); + } else { + obj = jp.nextTextValue(); + } + + if (null != obj) { + if (!ClassUtils.isAssignable(obj.getClass(), targetType)) { + obj = conversionService.convert(obj, targetType); + } + + try { + setter.invoke(bean, obj); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + + } + + return bean; + } + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java new file mode 100644 index 000000000..ce2130e64 --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java @@ -0,0 +1,75 @@ +package org.springframework.data.rest.core.util; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.map.SerializerProvider; +import org.codehaus.jackson.map.ser.std.SerializerBase; +import org.springframework.util.ClassUtils; + +/** + * @author Jon Brisbin + */ +public class FluentBeanSerializer extends SerializerBase { + + @SuppressWarnings({"unchecked"}) + public FluentBeanSerializer(final Class t) { + super(t); + + if (!FluentBeanUtils.isFluentBean(t)) { + throw new IllegalArgumentException("Class of type " + t + " is not a FluentBean"); + } + } + + @SuppressWarnings({"unchecked"}) + @Override + public void serialize(final Object value, + final JsonGenerator jgen, + final SerializerProvider provider) + throws IOException, + JsonGenerationException { + if (null == value) { + provider.defaultSerializeNull(jgen); + } else { + Class type = value.getClass(); + if (ClassUtils.isAssignable(type, Collection.class)) { + jgen.writeStartArray(); + for (Object o : (Collection) value) { + write(o, jgen, provider); + } + jgen.writeEndArray(); + } else if (ClassUtils.isAssignable(type, Map.class)) { + jgen.writeStartObject(); + for (Map.Entry entry : ((Map) value).entrySet()) { + jgen.writeFieldName(entry.getKey()); + write(entry.getValue(), jgen, provider); + } + jgen.writeEndObject(); + } else { + write(value, jgen, provider); + } + } + } + + private void write(final Object value, + final JsonGenerator jgen, + final SerializerProvider provider) throws IOException { + Class type = value.getClass(); + if (ClassUtils.isAssignable(type, _handledType)) { + jgen.writeStartObject(); + for (String fname : FluentBeanUtils.metadata(type).fieldNames()) { + jgen.writeFieldName(fname); + write(FluentBeanUtils.get(fname, value), jgen, provider); + } + jgen.writeEndObject(); + } else { + jgen.writeObject(value); + } + } + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java new file mode 100644 index 000000000..075304fd8 --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java @@ -0,0 +1,132 @@ +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.ReflectionUtils; + +/** + * @author Jon Brisbin + */ +public abstract class FluentBeanUtils { + + private static final Logger log = LoggerFactory.getLogger(FluentBeanUtils.class); + private static final LoadingCache, Metadata> metadata = CacheBuilder.newBuilder().build( + new CacheLoader, 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)) { + 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); + } + } + }); + } + } + } + ); + return meta; + } + } + ); + + public static Metadata metadata(Class targetType) { + try { + return metadata.get(targetType); + } catch (ExecutionException e) { + throw new IllegalStateException(e); + } + } + + 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); + } else { + return null; + } + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.debug(t.getMessage(), t); + } + return 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); + } else { + return null; + } + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.debug(t.getMessage(), t); + } + return null; + } + } + + 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 fieldNames = new ArrayList(); + Map getters = new HashMap(); + Map setters = new HashMap(); + + public List fieldNames() { + return fieldNames; + } + + public Map getters() { + return getters; + } + + public Map setters() { + return setters; + } + } + +} diff --git a/core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java b/core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java new file mode 100644 index 000000000..0be37378a --- /dev/null +++ b/core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java @@ -0,0 +1,118 @@ +package org.springframework.data.rest.core.util; + +import java.net.URI; +import java.util.List; +import java.util.Stack; + +import org.springframework.data.rest.core.Handler; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +public abstract class UriUtils { + + private UriUtils() { + } + + public static boolean validBaseUri(URI baseUri, URI uri) { + String path = UriUtils.path(baseUri.relativize(uri)); + return !StringUtils.hasText(path) || path.charAt(0) != '/'; + } + + public static V foreach(URI baseUri, URI uri, Handler handler) { + List uris = explode(baseUri, uri); + V v = null; + for (URI u : uris) { + v = handler.handle(u); + } + return v; + } + + public static Stack explode(URI baseUri, URI uri) { + Stack uris = new Stack(); + if (StringUtils.hasText(uri.getPath())) { + URI relativeUri = baseUri.relativize(uri); + if (StringUtils.hasText(relativeUri.getPath())) { + for (String part : relativeUri.getPath().split("/")) { + uris.add(URI.create(part + (StringUtils.hasText(uri.getQuery()) ? "?" + uri.getQuery() : ""))); + } + } + } + return uris; + } + + public static URI merge(URI baseUri, URI... uris) { + StringBuilder query = new StringBuilder(); + + UriComponentsBuilder ub = UriComponentsBuilder.fromUri(baseUri); + for (URI uri : uris) { + String s = uri.getScheme(); + if (null != s) { + ub.scheme(s); + } + + s = uri.getUserInfo(); + if (null != s) { + ub.userInfo(s); + } + + s = uri.getHost(); + if (null != s) { + ub.host(s); + } + + int i = uri.getPort(); + if (i > 0) { + ub.port(i); + } + + s = uri.getPath(); + if (null != s) { + if (!uri.isAbsolute() && StringUtils.hasText(s)) { + ub.pathSegment(s); + } else { + ub.path(s); + } + } + + s = uri.getQuery(); + if (null != s) { + if (query.length() > 0) { + query.append("&"); + } + query.append(s); + } + + s = uri.getFragment(); + if (null != s) { + ub.fragment(s); + } + } + + if (query.length() > 0) { + ub.query(query.toString()); + } + + return ub.build().toUri(); + } + + public static String path(URI uri) { + if (null == uri) { + return null; + } + String s = uri.getPath(); + if (s.endsWith("/")) { + return s.substring(0, s.length() - 1); + } else { + return s; + } + } + + public static URI tail(URI baseUri, URI uri) { + Stack uris = explode(baseUri, uri); + return uris.size() > 0 ? uris.get(Math.max(uris.size() - 1, 0)) : null; + } + +} diff --git a/core/src/test/groovy/org/springframework/data/rest/core/UriUtilsSpec.groovy b/core/src/test/groovy/org/springframework/data/rest/core/UriUtilsSpec.groovy new file mode 100644 index 000000000..60910deb8 --- /dev/null +++ b/core/src/test/groovy/org/springframework/data/rest/core/UriUtilsSpec.groovy @@ -0,0 +1,48 @@ +package org.springframework.data.rest.core + +import org.springframework.data.rest.core.util.UriUtils +import spock.lang.Specification + +/** + * @author Jon Brisbin + */ +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" + + } + +} diff --git a/core/src/test/resources/logback.xml b/core/src/test/resources/logback.xml new file mode 100644 index 000000000..919ca3f93 --- /dev/null +++ b/core/src/test/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..d6c71179f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,25 @@ +# Logging +slf4jVersion = 1.6.4 +logbackVersion = 1.0.0 + +# Libraries +springVersion = 3.1.1.RELEASE +cglibVersion = 2.2 + +# Languages +groovyVersion = 1.8.6 + +# Supporting libraries +sdCommonsVersion = 1.2.0.RELEASE +sdJpaVersion = 1.0.1.RELEASE +jacksonVersion = 1.9.2 +hibernateVersion = 3.5.6-Final + +# Testing +spockVersion = 0.5-groovy-1.8 + +## OSGi ranges +spring.range = "[3.1.1, 4.0.0)" +jackson.range = "[1.9, 2.0.0)" + +sdRestVersion = 1.0.0.BUILD-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1182d0584 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..12d9a1cac --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Mar 08 09:45:33 CST 2012 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://repo.gradle.org/gradle/distributions/gradle-1.0-milestone-8-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..ae91ed902 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/bin/bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/repository/build.gradle b/repository/build.gradle new file mode 100644 index 000000000..4b8a8c85a --- /dev/null +++ b/repository/build.gradle @@ -0,0 +1,22 @@ +dependencies { + + // Spring + compile "org.springframework:spring-orm:$springVersion" + compile "org.springframework:spring-oxm:$springVersion" + compile "org.springframework:spring-tx:$springVersion" + + // JPA + compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" + + // Spring Data + compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion" + compile "org.springframework.data:spring-data-jpa:$sdJpaVersion" + + // Exporter core + compile project(":core") + + // Testing + testCompile "org.hibernate:hibernate-entitymanager:$hibernateVersion" + testCompile "org.hsqldb:hsqldb:1.8.0.10" + +} diff --git a/repository/src/main/java/org/springframework/data/rest/repository/JpaEntityMetadata.java b/repository/src/main/java/org/springframework/data/rest/repository/JpaEntityMetadata.java new file mode 100644 index 000000000..0fe82f32b --- /dev/null +++ b/repository/src/main/java/org/springframework/data/rest/repository/JpaEntityMetadata.java @@ -0,0 +1,145 @@ +package org.springframework.data.rest.repository; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; + +import org.springframework.data.rest.core.Handler; +import org.springframework.util.ReflectionUtils; + +/** + * @author Jon Brisbin + */ +public class JpaEntityMetadata { + + private Class targetType; + private Map embeddedAttributes = new HashMap(); + private Map fields = new HashMap(); + private Map linkedAttributes = new HashMap(); + private final Attribute idAttribute; + private final Attribute versionAttribute; + + @SuppressWarnings({"unchecked"}) + public JpaEntityMetadata(EntityType entityType, JpaRepositoryMetadata repositoryMetadata) { + targetType = entityType.getJavaType(); + + Attribute idAttribute = entityType.getId(entityType.getIdType().getJavaType()); + Attribute versionAttribute = entityType.getVersion(Long.class); + for (Attribute attr : (Set) entityType.getAttributes()) { + String name = attr.getName(); + Field f = ReflectionUtils.findField(targetType, attr.getName()); + ReflectionUtils.makeAccessible(f); + fields.put(name, f); + + if (attr instanceof SingularAttribute) { + SingularAttribute sattr = (SingularAttribute) attr; + if (null != repositoryMetadata.repositoryFor(attr.getJavaType())) { + linkedAttributes.put(name, attr); + } else if (!sattr.isId() && !sattr.isVersion()) { + embeddedAttributes.put(name, attr); + } + } else if (attr instanceof PluralAttribute) { + PluralAttribute pattr = (PluralAttribute) attr; + if (pattr.getElementType() instanceof EntityType + && null != repositoryMetadata.repositoryFor(pattr.getElementType().getJavaType())) { + linkedAttributes.put(name, attr); + } else { + embeddedAttributes.put(name, attr); + } + } + } + + this.idAttribute = idAttribute; + this.versionAttribute = versionAttribute; + } + + public Class targetType() { + return targetType; + } + + public Map embeddedAttributes() { + return Collections.unmodifiableMap(embeddedAttributes); + } + + public Map linkedAttributes() { + return Collections.unmodifiableMap(linkedAttributes); + } + + public Attribute idAttribute() { + return idAttribute; + } + + public Attribute versionAttribute() { + return versionAttribute; + } + + public void id(Serializable id, Object target) { + set(idAttribute.getName(), id, target); + } + + public Object id(Object target) { + return get(idAttribute.getName(), target); + } + + public Object version(Object target) { + return (null != versionAttribute ? get(versionAttribute.getName(), target) : null); + } + + public V doWithEmbedded(Handler handler) { + if (null == handler) { + return null; + } + V v = null; + for (Attribute attr : embeddedAttributes.values()) { + v = handler.handle(attr); + } + return v; + } + + public V doWithLinked(String name, Handler handler) { + if (null == handler) { + return null; + } + V v = null; + Attribute attr = linkedAttributes.get(name); + if (null != attr) { + v = handler.handle(attr); + } + return v; + } + + public V doWithLinked(Handler handler) { + if (null == handler) { + return null; + } + V v = null; + for (Attribute attr : linkedAttributes.values()) { + v = handler.handle(attr); + } + return v; + } + + public Object get(String name, Object target) { + try { + return fields.get(name).get(target); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + public void set(String name, Object arg, Object target) { + try { + fields.get(name).set(target, arg); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java b/repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java new file mode 100644 index 000000000..4d797ffb5 --- /dev/null +++ b/repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java @@ -0,0 +1,166 @@ +package org.springframework.data.rest.repository; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.Metamodel; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * @author Jon Brisbin + */ +public class JpaRepositoryMetadata implements InitializingBean, ApplicationContextAware { + + private ApplicationContext applicationContext; + private Map, RepositoryCacheEntry> repositories = new HashMap, RepositoryCacheEntry>(); + private EntityManager entityManager; + private Metamodel metamodel; + + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + this.entityManager = entityManager; + this.metamodel = entityManager.getMetamodel(); + } + + public CrudRepository repositoryFor(String name) { + if (null != name) { + for (Map.Entry, RepositoryCacheEntry> entry : repositories.entrySet()) { + if (name.equals(repositoryNameFor(entry.getValue().repository))) { + return entry.getValue().repository; + } + } + } + return null; + } + + public CrudRepository repositoryFor(Class domainClass) { + RepositoryCacheEntry entry = repositories.get(domainClass); + if (null != entry) { + return entry.repository; + } + return null; + } + + public EntityInformation entityInfoFor(Class domainClass) { + RepositoryCacheEntry entry = repositories.get(domainClass); + if (null != entry) { + return entry.entityInfo; + } + return null; + } + + public EntityType entityTypeFor(Class domainClass) { + return metamodel.entity(domainClass); + } + + public EntityInformation entityInfoFor(CrudRepository repository) { + for (Map.Entry, RepositoryCacheEntry> entry : repositories.entrySet()) { + if (entry.getValue().repository == repository) { + return entry.getValue().entityInfo; + } + } + return null; + } + + public JpaEntityMetadata entityMetadataFor(Class domainClass) { + RepositoryCacheEntry entry = repositories.get(domainClass); + if (null == entry.entityMetadata) { + entry.entityMetadata = new JpaEntityMetadata(metamodel.entity(domainClass), this); + } + return entry.entityMetadata; + } + + public String repositoryNameFor(Class domainClass) { + RepositoryCacheEntry entry = repositories.get(domainClass); + if (null != entry) { + return entry.name; + } + return null; + } + + public String repositoryNameFor(CrudRepository repository) { + for (Map.Entry, RepositoryCacheEntry> entry : repositories.entrySet()) { + if (entry.getValue().repository == repository) { + return entry.getValue().name; + } + } + return null; + } + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public List repositoryNames() { + List names = new ArrayList(); + for (Map.Entry, RepositoryCacheEntry> entry : repositories.entrySet()) { + names.add(entry.getValue().name); + } + return names; + } + + public void setRepositories(Collection repositories) { + for (CrudRepository repository : repositories) { + Class repoClass = AopUtils.getTargetClass(repository); + Field infoField = ReflectionUtils.findField(repoClass, "entityInformation"); + ReflectionUtils.makeAccessible(infoField); + Method m = ReflectionUtils.findMethod(repository.getClass(), "getTargetSource"); + ReflectionUtils.makeAccessible(m); + try { + SingletonTargetSource targetRepo = (SingletonTargetSource) m.invoke(repository); + EntityInformation entityInfo = (EntityInformation) infoField.get(targetRepo.getTarget()); + Class[] intfs = repository.getClass().getInterfaces(); + String name = StringUtils.uncapitalize(intfs[0].getSimpleName().replaceAll("Repository", "")); + this.repositories.put(entityInfo.getJavaType(), new RepositoryCacheEntry(name, repository, entityInfo, null)); + } catch (Throwable t) { + throw new IllegalStateException(t); + } + } + } + + @Override public void afterPropertiesSet() throws Exception { + if (this.repositories.isEmpty()) { + ApplicationContext appCtx = applicationContext; + while (null != appCtx) { + Map beans = appCtx.getBeansOfType(CrudRepository.class); + setRepositories(beans.values()); + appCtx = appCtx.getParent(); + } + } + } + + private class RepositoryCacheEntry { + String name; + CrudRepository repository; + EntityInformation entityInfo; + JpaEntityMetadata entityMetadata; + + private RepositoryCacheEntry(String name, + CrudRepository repository, + EntityInformation entityInfo, + JpaEntityMetadata entityMetadata) { + this.name = name; + this.repository = repository; + this.entityInfo = entityInfo; + this.entityMetadata = entityMetadata; + } + } + +} diff --git a/rest/build.gradle b/rest/build.gradle new file mode 100644 index 000000000..afc9cf583 --- /dev/null +++ b/rest/build.gradle @@ -0,0 +1,22 @@ +apply plugin: "war" +apply plugin: "jetty" + +dependencies { + + // APIS + compile "javax.servlet:servlet-api:2.5" + + // JPA + compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" + compile "org.hibernate:hibernate-entitymanager:$hibernateVersion" + + // H2 + compile "org.hsqldb:hsqldb:1.8.0.10" + + // Spring + compile "org.springframework:spring-webmvc:$springVersion" + + // Repository Exporter support + compile project(":repository") + +} \ No newline at end of file diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/JsonView.java b/rest/src/main/java/org/springframework/data/rest/mvc/JsonView.java new file mode 100644 index 000000000..bbe4a4400 --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/JsonView.java @@ -0,0 +1,84 @@ +package org.springframework.data.rest.mvc; + +import java.io.ByteArrayOutputStream; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.ser.CustomSerializerFactory; +import org.springframework.data.rest.core.SimpleLink; +import org.springframework.data.rest.core.util.FluentBeanSerializer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.view.AbstractView; + +/** + * @author Jon Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class JsonView extends AbstractView { + + private ObjectMapper mapper = new ObjectMapper(); + + { + CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory(); + customSerializerFactory.addSpecificMapping(SimpleLink.class, new FluentBeanSerializer(SimpleLink.class)); + mapper.setSerializerFactory(customSerializerFactory); + } + + public JsonView(String mediaType) { + setContentType(mediaType); + } + + @Override + protected void renderMergedOutputModel(Map model, + HttpServletRequest request, + HttpServletResponse response) throws Exception { + HttpStatus status = status(model); + response.setStatus(status.value()); + + String contentType = getContentType(); + HttpHeaders headers = headers(model); + if (null != headers) { + for (Map.Entry entry : headers.toSingleValueMap().entrySet()) { + response.setHeader(entry.getKey(), entry.getValue()); + } + if (null != headers.getContentType()) { + contentType = headers.getContentType().toString(); + } + } + response.setContentType(contentType); + + Object resource = model.get("resource"); + if (null != resource) { + if (resource instanceof Throwable) { + resource = ((Throwable) resource).getMessage(); + } + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + mapper.writerWithDefaultPrettyPrinter().writeValue(bout, resource); + + response.getOutputStream().write(bout.toByteArray()); + } else { + response.setContentLength(0); + } + } + + + private HttpStatus status(Map model) { + Object o = model.get("status"); + if (null != o && o instanceof HttpStatus) { + return (HttpStatus) o; + } + throw new IllegalArgumentException("No status is set in the model."); + } + + private HttpHeaders headers(Map model) { + Object o = model.get("headers"); + if (null != o && o instanceof HttpHeaders) { + return (HttpHeaders) o; + } + return null; + } + +} diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestConfiguration.java b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestConfiguration.java new file mode 100644 index 000000000..1f5672703 --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestConfiguration.java @@ -0,0 +1,61 @@ +package org.springframework.data.rest.mvc; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.rest.repository.JpaRepositoryMetadata; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; + +/** + * @author Jon Brisbin + */ +@Configuration +@ImportResource("classpath*:META-INF/spring-data-rest/**/*-export.xml") +public class RepositoryRestConfiguration { + + @Autowired(required = false) + URI baseUri; + @Autowired(required = false) + JpaRepositoryMetadata jpaRepositoryMetadata; + @Autowired(required = false) + List> httpMessageConverters = new ArrayList>(); + + @Bean URI baseUri() { + if (null == baseUri) { + baseUri = URI.create(""); + } + return baseUri; + } + + @Bean List> httpMessageConverters() { + if (httpMessageConverters.isEmpty()) { + MappingJacksonHttpMessageConverter json = new MappingJacksonHttpMessageConverter(); + json.setSupportedMediaTypes( + Arrays.asList(MediaType.APPLICATION_JSON, MediaType.valueOf("application/x-spring-data+json")) + ); + httpMessageConverters.add(json); + } + return httpMessageConverters; + } + + @Bean JpaRepositoryMetadata jpaRepositoryMetadata() throws Exception { + if (null == jpaRepositoryMetadata) { + jpaRepositoryMetadata = new JpaRepositoryMetadata(); + } + return jpaRepositoryMetadata; + } + + @Bean PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() { + return new PersistenceAnnotationBeanPostProcessor(); + } + +} diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestController.java b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestController.java new file mode 100644 index 000000000..4633b112b --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestController.java @@ -0,0 +1,908 @@ +package org.springframework.data.rest.mvc; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.rest.core.Handler; +import org.springframework.data.rest.core.Link; +import org.springframework.data.rest.core.SimpleLink; +import org.springframework.data.rest.core.util.UriUtils; +import org.springframework.data.rest.repository.JpaEntityMetadata; +import org.springframework.data.rest.repository.JpaRepositoryMetadata; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +@Controller +public class RepositoryRestController implements InitializingBean { + + public static final String STATUS = "status"; + public static final String HEADERS = "headers"; + public static final String RESOURCE = "resource"; + public static final String SELF = "self"; + public static final String LINKS = "_links"; + + public static final int HAS_RESOURCE = 1; + public static final int HAS_RESOURCE_ID = 2; + public static final int HAS_SECOND_LEVEL_RESOURCE = 3; + public static final int HAS_SECOND_LEVEL_ID = 4; + + private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); + + private URI baseUri = URI.create("http://localhost:8080"); + private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list"); + private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json"); + private JpaRepositoryMetadata repositoryMetadata; + private ConversionService conversionService = new DefaultConversionService(); + private List> httpMessageConverters; + + public URI getBaseUri() { + return baseUri; + } + + public void setBaseUri(URI baseUri) { + this.baseUri = baseUri; + } + + public URI baseUri() { + return baseUri; + } + + public RepositoryRestController baseUri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + public JpaRepositoryMetadata getRepositoryMetadata() { + return repositoryMetadata; + } + + public void setRepositoryMetadata(JpaRepositoryMetadata repositoryMetadata) { + this.repositoryMetadata = repositoryMetadata; + } + + public JpaRepositoryMetadata repositoryMetadata() { + return repositoryMetadata; + } + + public RepositoryRestController repositoryMetadata(JpaRepositoryMetadata repositoryMetadata) { + this.repositoryMetadata = repositoryMetadata; + return this; + } + + public ConversionService getConversionService() { + return conversionService; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public ConversionService conversionService() { + return conversionService; + } + + public RepositoryRestController conversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + public List> getHttpMessageConverters() { + return httpMessageConverters; + } + + public void setHttpMessageConverters(List> httpMessageConverters) { + this.httpMessageConverters = httpMessageConverters; + } + + public List> httpMessageConverters() { + return httpMessageConverters; + } + + public RepositoryRestController httpMessageConverters(List> httpMessageConverters) { + this.httpMessageConverters = httpMessageConverters; + return this; + } + + public MediaType getUriListMediaType() { + return uriListMediaType; + } + + public void setUriListMediaType(MediaType uriListMediaType) { + this.uriListMediaType = uriListMediaType; + } + + public void setUriListMediaType(String uriListMediaType) { + this.uriListMediaType = MediaType.valueOf(uriListMediaType); + } + + public MediaType uriListMediaType() { + return uriListMediaType; + } + + public RepositoryRestController uriListMediaType(MediaType uriListMediaType) { + setUriListMediaType(uriListMediaType); + return this; + } + + public RepositoryRestController uriListMediaType(String uriListMediaType) { + setUriListMediaType(uriListMediaType); + return this; + } + + public MediaType getJsonMediaType() { + return jsonMediaType; + } + + public void setJsonMediaType(MediaType jsonMediaType) { + this.jsonMediaType = jsonMediaType; + } + + public void setJsonMediaType(String jsonMediaType) { + this.jsonMediaType = MediaType.valueOf(jsonMediaType); + } + + public MediaType jsonMediaType() { + return jsonMediaType; + } + + public RepositoryRestController jsonMediaType(MediaType jsonMediaType) { + setJsonMediaType(jsonMediaType); + return this; + } + + public RepositoryRestController jsonMediaType(String jsonMediaType) { + setJsonMediaType(jsonMediaType); + return this; + } + + @Override public void afterPropertiesSet() throws Exception { + Assert.notNull(httpMessageConverters, "HttpMessageConverters cannot be null"); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping(method = RequestMethod.GET) + public void get(ServerHttpRequest request, final Model model) { + if (validBaseUri(request.getURI())) { + + URI relativeUri = baseUri.relativize(request.getURI()); + final Stack uris = UriUtils.explode(baseUri, relativeUri); + if (LOG.isDebugEnabled()) { + LOG.debug("uris: " + uris); + } + + final int uriCnt = uris.size(); + if (uris.size() > 0) { + final String repoName = uris.get(0).getPath(); + final CrudRepository repo = repositoryMetadata.repositoryFor(repoName); + if (null == repo) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + final EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo); + final Class domainClass = entityInfo.getJavaType(); + final Class idType = entityInfo.getIdType(); + final EntityType entityType = repositoryMetadata.entityTypeFor(domainClass); + final JpaEntityMetadata entityMetadata = repositoryMetadata.entityMetadataFor(domainClass); + + switch (uriCnt) { + + // List the entities + case HAS_RESOURCE: { + Map> resource = new HashMap>(); + List links = new ArrayList(); + Iterator iter = repo.findAll().iterator(); + while (iter.hasNext()) { + Object o = iter.next(); + Serializable id = entityInfo.getId(o); + links.add(new SimpleLink(o.getClass().getSimpleName(), + UriComponentsBuilder.fromUri(baseUri) + .pathSegment(repoName, id.toString()) + .build() + .toUri()) + ); + } + resource.put(LINKS, links); + + model.addAttribute(STATUS, HttpStatus.OK); + model.addAttribute(RESOURCE, resource); + return; + } + + // Retrieve an entity + case HAS_RESOURCE_ID: { + final String sId = UriUtils.path(uris.get(1)); + Serializable serId; + if (idType == String.class) { + serId = sId; + } else { + serId = conversionService.convert(sId, idType); + } + + final Object entity = repo.findOne(serId); + if (null == entity) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { + Map entityDto = extractPropertiesLinkAware(entity, entityMetadata, repoName, sId); + addSelfLink(entityDto, repoName, sId); + + model.addAttribute(STATUS, HttpStatus.OK); + model.addAttribute(RESOURCE, entityDto); + } + return; + } + + // Retrieve the linked entities + case HAS_SECOND_LEVEL_RESOURCE: + // Retrieve a child entity + case HAS_SECOND_LEVEL_ID: { + final String sId = UriUtils.path(uris.get(1)); + final Serializable serId; + if (idType == String.class) { + serId = sId; + } else { + serId = conversionService.convert(sId, idType); + } + + Object entity = repo.findOne(serId); + if (null == entity) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { + model.addAttribute(STATUS, HttpStatus.OK); + final String attrName = UriUtils.path(uris.get(2)); + Attribute attr = entityType.getAttribute(attrName); + if (null != attr) { + Class childType; + if (attr instanceof PluralAttribute) { + childType = ((PluralAttribute) attr).getElementType().getJavaType(); + } else { + childType = attr.getJavaType(); + } + final CrudRepository childRepo = repositoryMetadata.repositoryFor(childType); + if (null == childRepo) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + final EntityInformation childEntityInfo = repositoryMetadata.entityInfoFor(childRepo); + final JpaEntityMetadata childEntityMetadata = repositoryMetadata.entityMetadataFor(childEntityInfo.getJavaType()); + + final Object child = entityMetadata.get(attrName, entity); + if (uriCnt == 3) { + Map resource = new HashMap(); + if (null != child) { + if (child instanceof Collection) { + List links = new ArrayList(); + for (Object o : (Collection) child) { + String childId = childEntityInfo.getId(o).toString(); + URI uri = UriComponentsBuilder.fromUri(baseUri) + .pathSegment(repoName, sId, attrName, childId) + .build() + .toUri(); + links.add(new SimpleLink(childType.getSimpleName(), uri)); + } + resource.put(LINKS, links); + model.addAttribute(RESOURCE, resource); + } else if (child instanceof Map) { + List links = new ArrayList(); + for (Map.Entry entry : ((Map) child).entrySet()) { + String childId = childEntityInfo.getId(entry.getValue()).toString(); + URI uri = UriComponentsBuilder.fromUri(baseUri) + .pathSegment(repoName, sId, attrName, childId) + .build() + .toUri(); + Object oKey = entry.getKey(); + String sKey; + if (ClassUtils.isAssignable(oKey.getClass(), String.class)) { + sKey = (String) oKey; + } else { + sKey = conversionService.convert(oKey, String.class); + } + links.add(new SimpleLink(sKey, uri)); + } + resource.put(attrName, links); + model.addAttribute(RESOURCE, resource); + } else { + model.addAttribute(RESOURCE, child); + } + } + } else if (uriCnt == 4) { + final String childId = UriUtils.path(uris.get(3)); + Class childIdType = childEntityInfo.getIdType(); + final Serializable childSerId; + if (idType == String.class) { + childSerId = childId; + } else { + childSerId = conversionService.convert(childId, childIdType); + } + + final Object o = childRepo.findOne(childSerId); + if (null != o) { + Map entityDto = extractPropertiesLinkAware(o, + childEntityMetadata, + repoName, + sId, + attrName); + addSelfLink(entityDto, repositoryMetadata.repositoryNameFor(childRepo), childId); + model.addAttribute(RESOURCE, entityDto); + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } + } + } + } + + return; + } + + // List the repositories + default: + } + } + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + + model.addAttribute(STATUS, HttpStatus.OK); + + Map> resource = new HashMap>(); + List links = new ArrayList(); + for (String name : repositoryMetadata.repositoryNames()) { + links.add(new SimpleLink(name, + UriComponentsBuilder.fromUri(baseUri) + .pathSegment(name) + .build() + .toUri()) + ); + } + resource.put(LINKS, links); + + model.addAttribute(RESOURCE, resource); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping(method = {RequestMethod.POST, RequestMethod.PUT}) + public void createOrUpdate(ServerHttpRequest request, Model model) { + if (validBaseUri(request.getURI())) { + + URI relativeUri = baseUri.relativize(request.getURI()); + final Stack uris = UriUtils.explode(baseUri, relativeUri); + if (LOG.isDebugEnabled()) { + LOG.debug("uris: " + uris); + } + + final int uriCnt = uris.size(); + if (uriCnt > 0) { + final String repoName = UriUtils.path(uris.get(0)); + final CrudRepository repo = repositoryMetadata.repositoryFor(repoName); + if (null == repo) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + final EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo); + final Class domainClass = entityInfo.getJavaType(); + final Class idType = entityInfo.getIdType(); + final JpaEntityMetadata entityMetadata = repositoryMetadata.entityMetadataFor(domainClass); + final MediaType incomingMediaType = request.getHeaders().getContentType(); + + switch (uriCnt) { + + // Create a new entity + case HAS_RESOURCE: + case HAS_RESOURCE_ID: { + if (incomingMediaType.equals(jsonMediaType)) { + try { + final Map incoming = readIncoming(request, incomingMediaType, Map.class); + if (null == incoming) { + model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE); + } else { + String resourceId; + Serializable serId = null; + if (uriCnt == HAS_RESOURCE_ID) { + resourceId = UriUtils.path(uris.get(1)); + if (idType == String.class) { + serId = resourceId; + } else { + serId = conversionService.convert(resourceId, idType); + } + } + + final Object entity = request.getMethod() == HttpMethod.PUT ? + repo.findOne(serId) : + entityMetadata.targetType().newInstance(); + + entityMetadata.doWithEmbedded(new Handler() { + @Override public Void handle(Attribute attribute) { + String name = attribute.getName(); + if (incoming.containsKey(name)) { + Object val = incoming.get(name); + entityMetadata.set(name, val, entity); + } + return null; + } + }); + + if (uriCnt == HAS_RESOURCE_ID && request.getMethod() == HttpMethod.POST) { + entityMetadata.id(serId, entity); + } + + Object savedEntity = repo.save(entity); + String sId = entityInfo.getId(savedEntity).toString(); + + URI selfUri = UriComponentsBuilder.fromUri(baseUri) + .pathSegment(repoName, sId) + .build() + .toUri(); + + if (request.getMethod() == HttpMethod.POST) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Location", selfUri.toString()); + model.addAttribute(HEADERS, headers); + model.addAttribute(STATUS, HttpStatus.CREATED); + } else { + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + } + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); + } + return; + } else { + model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE); + return; + } + } + + case HAS_SECOND_LEVEL_RESOURCE: { + String propertyName = UriUtils.path(uris.get(2)); + Attribute attr = entityMetadata.linkedAttributes().get(propertyName); + if (null != attr) { + Object entity = resolveTopLevelResource(request.getURI().toString()); + if (null != entity) { + try { + if (incomingMediaType.equals(uriListMediaType)) { + BufferedReader in = new BufferedReader(new InputStreamReader(request.getBody())); + String line; + while (null != (line = in.readLine())) { + String sLinkUri = line.trim(); + Object childEntity = resolveTopLevelResource(sLinkUri); + + if (attr instanceof PluralAttribute) { + PluralAttribute plAttr = (PluralAttribute) attr; + switch (plAttr.getCollectionType()) { + case COLLECTION: + case LIST: + if (request.getMethod() == HttpMethod.PUT) { + entityMetadata.set(propertyName, new ArrayList(), entity); + } + addToCollection(propertyName, entity, entityMetadata, ArrayList.class, childEntity); + break; + case SET: + if (request.getMethod() == HttpMethod.PUT) { + entityMetadata.set(propertyName, new HashSet(), entity); + } + addToCollection(propertyName, entity, entityMetadata, HashSet.class, childEntity); + break; + case MAP: + model.addAttribute(STATUS, HttpStatus.UNSUPPORTED_MEDIA_TYPE); + return; + } + } else if (attr instanceof SingularAttribute) { + entityMetadata.set(propertyName, childEntity, entity); + } + } + repo.save(entity); + if (request.getMethod() == HttpMethod.PUT) { + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + } else { + model.addAttribute(STATUS, HttpStatus.CREATED); + } + } else if (incomingMediaType.equals(jsonMediaType)) { + final List> incoming = readIncoming(request, incomingMediaType, List.class); + for (Map link : incoming) { + String sLinkUri = link.get("href"); + Object childEntity = resolveTopLevelResource(sLinkUri); + + if (attr instanceof PluralAttribute) { + PluralAttribute plAttr = (PluralAttribute) attr; + switch (plAttr.getCollectionType()) { + case COLLECTION: + case LIST: + if (request.getMethod() == HttpMethod.PUT) { + entityMetadata.set(propertyName, new ArrayList(), entity); + } + addToCollection(propertyName, entity, entityMetadata, ArrayList.class, childEntity); + break; + case SET: + if (request.getMethod() == HttpMethod.PUT) { + entityMetadata.set(propertyName, new HashSet(), entity); + } + addToCollection(propertyName, entity, entityMetadata, HashSet.class, childEntity); + break; + case MAP: + if (request.getMethod() == HttpMethod.PUT) { + entityMetadata.set(propertyName, new HashMap(), entity); + } + addToMap(propertyName, entity, entityMetadata, link.get("rel"), childEntity); + break; + } + } else if (attr instanceof SingularAttribute) { + entityMetadata.set(propertyName, childEntity, entity); + } + } + repo.save(entity); + if (request.getMethod() == HttpMethod.PUT) { + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + } else { + model.addAttribute(STATUS, HttpStatus.CREATED); + } + } + } catch (IOException e) { + model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR); + } catch (InstantiationException e) { + model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); + } catch (IllegalAccessException e) { + model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } + } + return; + } + + // List resources + default: + } + } + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + + model.addAttribute(STATUS, HttpStatus.OK); + + Map> resource = new HashMap>(); + List links = new ArrayList(); + for (String name : repositoryMetadata.repositoryNames()) { + links.add(new SimpleLink(name, + UriComponentsBuilder.fromUri(baseUri) + .pathSegment(name) + .build() + .toUri()) + ); + } + resource.put(LINKS, links); + + model.addAttribute(RESOURCE, resource); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping(method = RequestMethod.DELETE) + public void delete(ServerHttpRequest request, Model model) { + if (validBaseUri(request.getURI())) { + + URI relativeUri = baseUri.relativize(request.getURI()); + Stack uris = UriUtils.explode(baseUri, relativeUri); + if (LOG.isDebugEnabled()) { + LOG.debug("uris: " + uris); + } + + int uriCnt = uris.size(); + if (uriCnt > 0) { + String repoName = UriUtils.path(uris.get(0)); + CrudRepository repo = repositoryMetadata.repositoryFor(repoName); + if (null == repo) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo); + Class domainClass = entityInfo.getJavaType(); + Class idType = entityInfo.getIdType(); + JpaEntityMetadata entityMetadata = repositoryMetadata.entityMetadataFor(domainClass); + + switch (uriCnt) { + + case HAS_RESOURCE_ID: { + String resourceId = UriUtils.path(uris.get(1)); + Serializable serId = conversionService.convert(resourceId, idType); + repo.delete(serId); + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + return; + } + + case HAS_SECOND_LEVEL_ID: { + String resourceId = UriUtils.path(uris.get(1)); + Serializable serId = conversionService.convert(resourceId, idType); + + Object entity = repo.findOne(serId); + + String propertyName = UriUtils.path(uris.get(2)); + Attribute attr = entityMetadata.linkedAttributes().get(propertyName); + if (null != attr && null != entity) { + Object childEntity = resolveSecondLevelResource(request.getURI().toString()); + if (attr instanceof PluralAttribute) { + PluralAttribute plAttr = (PluralAttribute) attr; + switch (plAttr.getCollectionType()) { + case COLLECTION: + case LIST: + case SET: + removeFromCollection(propertyName, entity, entityMetadata, childEntity); + break; + case MAP: + removeFromMap(propertyName, entity, entityMetadata, childEntity); + break; + } + } else { + entityMetadata.set(propertyName, null, entity); + } + + repo.save(entity); + + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + return; + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + } + + } + } + } else { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + return; + } + + model.addAttribute(STATUS, HttpStatus.OK); + + Map> resource = new HashMap>(); + List links = new ArrayList(); + for (String name : repositoryMetadata.repositoryNames()) { + links.add(new SimpleLink(name, + UriComponentsBuilder.fromUri(baseUri) + .pathSegment(name) + .build() + .toUri()) + ); + } + resource.put(LINKS, links); + + model.addAttribute(RESOURCE, resource); + } + + + private boolean validBaseUri(URI requestUri) { + String path = baseUri.relativize(requestUri).getPath(); + return !StringUtils.hasText(path) || path.charAt(0) != '/'; + } + + @SuppressWarnings({"unchecked"}) + private void addSelfLink(Map model, String... pathComponents) { + List links = (List) model.get(LINKS); + if (null == links) { + links = new ArrayList(); + model.put(LINKS, links); + } + URI selfUri = UriComponentsBuilder.fromUri(baseUri) + .pathSegment(pathComponents) + .build() + .toUri(); + links.add(new SimpleLink(SELF, selfUri)); + } + + @SuppressWarnings({"unchecked"}) + private V stringToSerializable(String s, Class targetType) { + if (ClassUtils.isAssignable(targetType, String.class)) { + return (V) s; + } else { + return conversionService.convert(s, targetType); + } + } + + @SuppressWarnings({"unchecked"}) + private V readIncoming(HttpInputMessage request, MediaType incomingMediaType, Class targetType) throws IOException { + for (HttpMessageConverter converter : httpMessageConverters) { + if (converter.canRead(targetType, incomingMediaType)) { + return (V) converter.read(targetType, request); + } + } + return null; + } + + @SuppressWarnings({"unchecked"}) + private Object resolveTopLevelResource(String uri) { + URI href = URI.create(uri); + if (validBaseUri(href)) { + URI relativeUri = baseUri.relativize(href); + Stack uris = UriUtils.explode(baseUri, relativeUri); + + if (uris.size() > 1) { + String repoName = UriUtils.path(uris.get(0)); + String sId = UriUtils.path(uris.get(1)); + + CrudRepository repo = repositoryMetadata.repositoryFor(repoName); + EntityInformation entityInfo = repositoryMetadata.entityInfoFor(repo); + Class idType = entityInfo.getIdType(); + + Serializable serId = stringToSerializable(sId, idType); + + return repo.findOne(serId); + } + } + return null; + + } + + @SuppressWarnings({"unchecked"}) + private Object resolveSecondLevelResource(String uri) { + URI href = URI.create(uri); + if (validBaseUri(href)) { + URI relativeUri = baseUri.relativize(href); + Stack uris = UriUtils.explode(baseUri, relativeUri); + + if (uris.size() > 3) { + String topLevelRepoName = UriUtils.path(uris.get(0)); + CrudRepository topLevelRepo = repositoryMetadata.repositoryFor(topLevelRepoName); + EntityInformation topLevelEntityInfo = repositoryMetadata.entityInfoFor(topLevelRepo); + JpaEntityMetadata topLevelEntityMetadata = repositoryMetadata.entityMetadataFor(topLevelEntityInfo.getJavaType()); + + String propertyName = UriUtils.path(uris.get(2)); + Attribute attr = topLevelEntityMetadata.linkedAttributes().get(propertyName); + if (null != attr) { + CrudRepository secondLevelRepo; + if (attr instanceof PluralAttribute) { + secondLevelRepo = repositoryMetadata.repositoryFor(((PluralAttribute) attr).getElementType().getJavaType()); + } else { + secondLevelRepo = repositoryMetadata.repositoryFor(attr.getJavaType()); + } + EntityInformation secondLevelEntityInfo = repositoryMetadata.entityInfoFor(secondLevelRepo); + Class secondLevelIdType = secondLevelEntityInfo.getIdType(); + Serializable secondLevelId = stringToSerializable(UriUtils.path(uris.get(3)), secondLevelIdType); + + return secondLevelRepo.findOne(secondLevelId); + } + } + } + return null; + } + + @SuppressWarnings({"unchecked"}) + private void addToCollection(String name, + Object entity, + JpaEntityMetadata metadata, + Class containerClass, + Object obj) + throws IllegalAccessException, + InstantiationException { + Collection c = (V) metadata.get(name, entity); + if (null == c) { + c = containerClass.newInstance(); + metadata.set(name, c, entity); + } + c.add(obj); + } + + public void removeFromCollection(String name, + Object entity, + JpaEntityMetadata metadata, + Object obj) { + Collection c = (Collection) metadata.get(name, entity); + if (null != c) { + c.remove(obj); + } + } + + @SuppressWarnings({"unchecked"}) + private void addToMap(String name, + Object entity, + JpaEntityMetadata metadata, + Object key, + Object obj) + throws IllegalAccessException, + InstantiationException { + Map m = (Map) metadata.get(name, entity); + if (null == m) { + m = new HashMap(); + metadata.set(name, m, entity); + } + m.put(key, obj); + } + + @SuppressWarnings({"unchecked"}) + public void removeFromMap(String name, + Object entity, + JpaEntityMetadata metadata, + Object obj) { + Map m = (Map) metadata.get(name, entity); + if (null != m) { + LOG.debug("obj: " + obj); + for (Map.Entry entry : m.entrySet()) { + LOG.debug("key: " + entry.getKey()); + LOG.debug("value: " + entry.getValue()); + LOG.debug("remove? " + (entry.getValue() == obj || entry.getValue().equals(obj))); + if (entry.getValue() == obj || entry.getValue().equals(obj)) { + m.remove(entry.getKey()); + } + } + } + } + + @SuppressWarnings({"unchecked"}) + private Map extractPropertiesLinkAware(final Object entity, + final JpaEntityMetadata entityMetadata, + final String... pathSegs) { + final Map entityDto = new HashMap(); + + entityMetadata.doWithEmbedded(new Handler() { + @Override public Void handle(Attribute attr) { + String name = attr.getName(); + Object val = entityMetadata.get(name, entity); + if (null != val) { + entityDto.put(name, val); + } + return null; + } + }); + + entityMetadata.doWithLinked(new Handler() { + @Override public Void handle(Attribute attr) { + String name = attr.getName(); + URI uri = UriComponentsBuilder.fromUri(baseUri) + .pathSegment(pathSegs) + .pathSegment(name) + .build() + .toUri(); + Link l = new SimpleLink(name, uri); + List links = (List) entityDto.get(LINKS); + if (null == links) { + links = new ArrayList(); + entityDto.put(LINKS, links); + } + links.add(l); + return null; + } + }); + + return entityDto; + } + +} diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestMvcConfiguration.java b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestMvcConfiguration.java new file mode 100644 index 000000000..6a4bfb31c --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/RepositoryRestMvcConfiguration.java @@ -0,0 +1,81 @@ +package org.springframework.data.rest.mvc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; + +/** + * @author Jon Brisbin + */ +@Configuration +public class RepositoryRestMvcConfiguration { + + @Autowired + RepositoryRestConfiguration repositoryRestConfiguration; + RepositoryRestController repositoryRestController; + + @Bean ContentNegotiatingViewResolver contentNegotiatingViewResolver() { + ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver(); + Map jsonTypes = new HashMap() {{ + put("json", "application/json"); + put("sdjson", "application/x-spring-data+json"); + put("urilist", "text/uri-list"); + }}; + + viewResolver.setMediaTypes(jsonTypes); + viewResolver.setDefaultViews( + Arrays.asList((View) new JsonView("application/json"), + (View) new JsonView("application/x-spring-data+json"), + (View) new UriListView()) + ); + return viewResolver; + } + + @Bean RepositoryRestController repositoryRestController() throws Exception { + if (null == repositoryRestController) { + this.repositoryRestController = new RepositoryRestController() + .baseUri(repositoryRestConfiguration.baseUri()) + .repositoryMetadata(repositoryRestConfiguration.jpaRepositoryMetadata()) + .httpMessageConverters(repositoryRestConfiguration.httpMessageConverters()) + .jsonMediaType("application/json"); + } + return repositoryRestController; + } + + @Bean RequestMappingHandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + @Bean RequestMappingHandlerAdapter handlerAdapter() { + RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); + handlerAdapter.setCustomArgumentResolvers( + Arrays.asList((HandlerMethodArgumentResolver) new ServerHttpRequestMethodArgumentResolver()) + ); + return handlerAdapter; + } + + @Bean ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { + return new ExceptionHandlerExceptionResolver(); + } + + @Bean DefaultHandlerExceptionResolver handlerExceptionResolver() { + return new DefaultHandlerExceptionResolver(); + } + + @Bean ResponseStatusExceptionResolver responseStatusExceptionResolver() { + return new ResponseStatusExceptionResolver(); + } + +} diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/ServerHttpRequestMethodArgumentResolver.java b/rest/src/main/java/org/springframework/data/rest/mvc/ServerHttpRequestMethodArgumentResolver.java new file mode 100644 index 000000000..6f292f9fb --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/ServerHttpRequestMethodArgumentResolver.java @@ -0,0 +1,31 @@ +package org.springframework.data.rest.mvc; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.MethodParameter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.ClassUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * @author Jon Brisbin + */ +public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override public boolean supportsParameter(MethodParameter parameter) { + return ClassUtils.isAssignable(parameter.getParameterType(), ServerHttpRequest.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + return new ServletServerHttpRequest((HttpServletRequest) webRequest.getNativeRequest()); + } + +} diff --git a/rest/src/main/java/org/springframework/data/rest/mvc/UriListView.java b/rest/src/main/java/org/springframework/data/rest/mvc/UriListView.java new file mode 100644 index 000000000..79f5563fb --- /dev/null +++ b/rest/src/main/java/org/springframework/data/rest/mvc/UriListView.java @@ -0,0 +1,68 @@ +package org.springframework.data.rest.mvc; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.data.rest.core.Link; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.view.AbstractView; + +/** + * @author Jon Brisbin + */ +public class UriListView extends AbstractView { + + public UriListView() { + setContentType("text/uri-list"); + } + + @SuppressWarnings({"unchecked"}) + @Override + protected void renderMergedOutputModel(Map model, + HttpServletRequest request, + HttpServletResponse response) throws Exception { + + Object resource = model.get("resource"); + response.setContentType(getContentType()); + + HttpStatus status = (HttpStatus) model.get("status"); + HttpHeaders headers = (HttpHeaders) model.get("headers"); + List links = null; + if (resource instanceof List) { + links = (List) resource; + } else if (resource instanceof Map) { + Map m = (Map) resource; + Object o = m.get("_links"); + if (null != o && o instanceof List) { + links = (List) o; + } else { + response.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + return; + } + } + + if (null != status) { + response.setStatus(status.value()); + } + + if (null != headers) { + for (Map.Entry entry : headers.toSingleValueMap().entrySet()) { + response.setHeader(entry.getKey(), entry.getValue()); + } + } + + PrintWriter out = response.getWriter(); + if (null != links) { + for (Link l : links) { + out.println(l.href().toString()); + } + } + out.flush(); + + } + +} diff --git a/rest/src/main/webapp/WEB-INF/repositories.xml b/rest/src/main/webapp/WEB-INF/repositories.xml new file mode 100644 index 000000000..efac5558f --- /dev/null +++ b/rest/src/main/webapp/WEB-INF/repositories.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/rest/src/main/webapp/WEB-INF/web.xml b/rest/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..ad7d31d4b --- /dev/null +++ b/rest/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,48 @@ + + + + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + org.springframework.data.rest.mvc.RepositoryRestConfiguration + + + org.springframework.web.context.ContextLoaderListener + + + + entityManagerInViewFilter + org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter + + + + exporter + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + org.springframework.data.rest.mvc.RepositoryRestMvcConfiguration + + 1 + + + + entityManagerInViewFilter + exporter + + + + exporter + /* + + + diff --git a/rest/src/test/java/org/springframework/data/rest/test/RestBuilder.java b/rest/src/test/java/org/springframework/data/rest/test/RestBuilder.java new file mode 100644 index 000000000..0dc9e4915 --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/RestBuilder.java @@ -0,0 +1,225 @@ +package org.springframework.data.rest.test; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import groovy.lang.Closure; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +/** + * @author Jon Brisbin + */ +public class RestBuilder { + + private static final String[] DATE_FORMATS = new String[]{ + "EEE, dd MMM yyyy HH:mm:ss z", + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", + "yyyy-MM-dd HH:mm:ss" + }; + + private ConversionService conversionService = new DefaultConversionService(); + private ClientHttpRequestFactory requestFactory; + private RestTemplate restTemplate; + private HttpHeaders headers = new HttpHeaders(); + private MediaType contentType; + private Class responseType = byte[].class; + private Map uriParams; + private Object body; + private Closure errorHandler; + + public RestBuilder() { + this.restTemplate = new RestTemplate(); + } + + public RestBuilder(ClientHttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + this.restTemplate = new RestTemplate(requestFactory); + } + + public Object call(Closure cl) { + RestBuilder b = null != requestFactory ? new RestBuilder(requestFactory) : new RestBuilder(); + if (null != errorHandler) { + b.setErrorHandler(errorHandler); + } + b.conversionService = conversionService; + cl.setDelegate(b); + + return cl.call(); + } + + public Object delete(String url) { + restTemplate.delete(url); + return this; + } + + @SuppressWarnings({"unchecked"}) + public Object get(String url) { + return restTemplate.getForEntity(maybeAddParams(url), responseType); + } + + @SuppressWarnings({"unchecked"}) + public Object post(String url) { + if (responseType == URI.class) { + return restTemplate.postForLocation(maybeAddParams(url), new HttpEntity(body, headers)); + } else { + return restTemplate.postForEntity(maybeAddParams(url), new HttpEntity(body, headers), responseType); + } + } + + @SuppressWarnings({"unchecked"}) + public Object put(String url) { + if (null != uriParams) { + restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers), uriParams); + } else { + restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers)); + } + return this; + } + + public Object accept(String accept) { + headers.setAccept(MediaType.parseMediaTypes(accept)); + return this; + } + + public Object body(Object body) { + this.body = body; + return this; + } + + public Object contentType(String contentType) { + this.contentType = MediaType.parseMediaType(contentType); + headers.setContentType(this.contentType); + return this; + } + + public Object date(Date date) { + headers.setDate(date.getTime()); + return this; + } + + @SuppressWarnings({"unchecked"}) + public Object date(String date) { + for (String fmt : DATE_FORMATS) { + try { + Date dte = new SimpleDateFormat(fmt).parse(date); + headers.setDate(dte.getTime()); + break; + } catch (ParseException e) {} + } + return this; + } + + @SuppressWarnings({"unchecked"}) + public Object header(String key, Object val) { + if (null != val) { + if (val instanceof List) { + headers.put(key, (List) val); + } else if (ClassUtils.isAssignable(val.getClass(), String.class)) { + headers.set(key, (String) val); + } else { + headers.set(key, conversionService.convert(val, String.class)); + } + } else { + headers.remove(key); + } + return this; + } + + @SuppressWarnings({"unchecked"}) + public Object headers(Map headers) { + this.headers.putAll(headers); + return this; + } + + public Date now() { + return Calendar.getInstance().getTime(); + } + + @SuppressWarnings({"unchecked"}) + public Object param(String key, String value) { + if (null == uriParams) { + uriParams = new HashMap(); + } + uriParams.put(key, value); + return this; + } + + public Object params(Map params) { + this.uriParams = params; + return this; + } + + public Object responseType(Class responseType) { + this.responseType = responseType; + return this; + } + + public Object setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + public Object setErrorHandler(Closure errorHandler) { + this.errorHandler = errorHandler; + if (null != errorHandler) { + this.restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override public void handleError(ClientHttpResponse response) throws IOException { + RestBuilder.this.errorHandler.call(response); + } + }); + } + return this; + } + + public Object setMessageConverters(List> converters) { + restTemplate.setMessageConverters(converters); + return this; + } + + @SuppressWarnings({"unchecked"}) + private String maybeAddParams(String url) { + StringBuffer buff = new StringBuffer(url); + if (null != uriParams) { + buff.append("?"); + for (Map.Entry entry : ((Map) uriParams).entrySet()) { + try { + buff.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + } + return buff.toString(); + } + + @Override public String toString() { + return "RestBuilder{" + + "requestFactory=" + requestFactory + + ", restTemplate=" + restTemplate + + ", headers=" + headers + + ", params=" + uriParams + + ", contentType=" + contentType + + ", errorHandler=" + errorHandler + + '}'; + } + +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/Address.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/Address.java new file mode 100644 index 000000000..9b5bfb546 --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/Address.java @@ -0,0 +1,61 @@ +package org.springframework.data.rest.test.mvc; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * @author Jon Brisbin + */ +@Entity +public class Address { + + @Id @GeneratedValue private Long id; + private String[] lines; + private String city; + private String province; + private String postalCode; + + public Address() { + } + + public Address(String[] lines, String city, String province, String postalCode) { + this.lines = lines; + this.city = city; + this.province = province; + this.postalCode = postalCode; + } + + public String[] getLines() { + return lines; + } + + public void setLines(String[] lines) { + this.lines = lines; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getPostalCode() { + return postalCode; + } + + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/AddressRepository.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/AddressRepository.java new file mode 100644 index 000000000..3e970fe16 --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/AddressRepository.java @@ -0,0 +1,9 @@ +package org.springframework.data.rest.test.mvc; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Jon Brisbin + */ +public interface AddressRepository extends CrudRepository { +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/Person.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/Person.java new file mode 100644 index 000000000..3d21b718a --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/Person.java @@ -0,0 +1,77 @@ +package org.springframework.data.rest.test.mvc; + +import java.util.List; +import java.util.Map; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Version; + +/** + * @author Jon Brisbin + */ +@Entity +public class Person { + + @Id private Long id; + private String name; + @Version + private Long version; + @OneToMany + private List
addresses; + @OneToMany + private Map profiles; + + public Person() { + } + + public Person(Long id, String name, List
addresses, Map profiles) { + this.id = id; + this.name = name; + this.addresses = addresses; + this.profiles = profiles; + } + + public Person(String name, List
addresses, Map profiles) { + this.name = name; + this.addresses = addresses; + this.profiles = profiles; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public List
getAddresses() { + return addresses; + } + + public void setAddresses(List
addresses) { + this.addresses = addresses; + } + + public Map getProfiles() { + return profiles; + } + + public void setProfiles(Map profiles) { + this.profiles = profiles; + } + +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonLoader.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonLoader.java new file mode 100644 index 000000000..d563839ce --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonLoader.java @@ -0,0 +1,63 @@ +package org.springframework.data.rest.test.mvc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jon Brisbin + */ +public class PersonLoader implements InitializingBean { + + private PersonRepository personRepository; + private ProfileRepository profileRepository; + private AddressRepository addressRepository; + + public PersonRepository getPersonRepository() { + return personRepository; + } + + public void setPersonRepository(PersonRepository personRepository) { + this.personRepository = personRepository; + } + + public ProfileRepository getProfileRepository() { + return profileRepository; + } + + public void setProfileRepository(ProfileRepository profileRepository) { + this.profileRepository = profileRepository; + } + + public AddressRepository getAddressRepository() { + return addressRepository; + } + + public void setAddressRepository(AddressRepository addressRepository) { + this.addressRepository = addressRepository; + } + + @Override public void afterPropertiesSet() throws Exception { + Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."}, "Univille", "ST", "12345")); + + Map pers1profiles = new HashMap(); + Profile twitter = profileRepository.save(new Profile("twitter", "#!/johndoe")); + Profile fb = profileRepository.save(new Profile("facebook", "/johndoe")); + pers1profiles.put("twitter", twitter); + pers1profiles.put("facebook", fb); + + personRepository.save(new Person(1L, "John Doe", Arrays.asList(pers1addr), pers1profiles)); + + Address pers2addr = addressRepository.save(new Address(new String[]{"1234 E. 2nd St."}, "Univille", "ST", "12345")); + + Map pers2profiles = new HashMap(); + Profile twitter2 = profileRepository.save(new Profile("twitter", "#!/janedoe")); + Profile fb2 = profileRepository.save(new Profile("facebook", "/janedoe")); + pers2profiles.put("facebook", fb2); + + personRepository.save(new Person(2L, "Jane Doe", Arrays.asList(pers2addr), pers2profiles)); + } + +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonRepository.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonRepository.java new file mode 100644 index 000000000..2a24995b2 --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/PersonRepository.java @@ -0,0 +1,9 @@ +package org.springframework.data.rest.test.mvc; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Jon Brisbin + */ +public interface PersonRepository extends CrudRepository { +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/Profile.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/Profile.java new file mode 100644 index 000000000..80ecb82bc --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/Profile.java @@ -0,0 +1,80 @@ +package org.springframework.data.rest.test.mvc; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * @author Jon Brisbin + */ +@Entity +public class Profile { + + @Id @GeneratedValue private Long id; + private String type; + private String url; + + public Profile() { + } + + public Profile(String type, String url) { + this.type = type; + this.url = url; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override public boolean equals(Object o) { + if (!(o instanceof Profile)) { + return false; + } + + Profile p2 = (Profile) o; + + boolean idEq; + if (null != id) { + idEq = id.equals(p2.id); + } else { + idEq = p2.id == null; + } + + boolean typeEq; + if (null != type) { + typeEq = type.equals(p2.type); + } else { + typeEq = p2.type == null; + } + + boolean urlEq; + if (null != url) { + urlEq = url.equals(p2.url); + } else { + urlEq = p2.url == null; + } + + return idEq && typeEq && urlEq; + } + + @Override public String toString() { + return "Profile{" + + "id=" + id + + ", type='" + type + '\'' + + ", url='" + url + '\'' + + '}'; + } + +} diff --git a/rest/src/test/java/org/springframework/data/rest/test/mvc/ProfileRepository.java b/rest/src/test/java/org/springframework/data/rest/test/mvc/ProfileRepository.java new file mode 100644 index 000000000..f1fba83bb --- /dev/null +++ b/rest/src/test/java/org/springframework/data/rest/test/mvc/ProfileRepository.java @@ -0,0 +1,9 @@ +package org.springframework.data.rest.test.mvc; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Jon Brisbin + */ +public interface ProfileRepository extends CrudRepository { +} diff --git a/rest/src/test/resouces/META-INF/persistence.xml b/rest/src/test/resouces/META-INF/persistence.xml new file mode 100644 index 000000000..d41aed326 --- /dev/null +++ b/rest/src/test/resouces/META-INF/persistence.xml @@ -0,0 +1,16 @@ + + + + org.springframework.data.rest.test.mvc.Person + org.springframework.data.rest.test.mvc.Profile + org.springframework.data.rest.test.mvc.Address + + + + + + + + + + \ No newline at end of file diff --git a/rest/src/test/resouces/META-INF/spring-data-rest/repositories-export.xml b/rest/src/test/resouces/META-INF/spring-data-rest/repositories-export.xml new file mode 100644 index 000000000..57ceb44e3 --- /dev/null +++ b/rest/src/test/resouces/META-INF/spring-data-rest/repositories-export.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rest/src/test/resouces/META-INF/spring-data-rest/shared.xml b/rest/src/test/resouces/META-INF/spring-data-rest/shared.xml new file mode 100644 index 000000000..522ed1555 --- /dev/null +++ b/rest/src/test/resouces/META-INF/spring-data-rest/shared.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rest/src/test/resouces/logback.xml b/rest/src/test/resouces/logback.xml new file mode 100644 index 000000000..77311749e --- /dev/null +++ b/rest/src/test/resouces/logback.xml @@ -0,0 +1,19 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..df8586390 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include "core", + "repository", + "rest"