diff --git a/.gitignore b/.gitignore index 021421903..549b071d5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ build/ .classpath .project .settings -.log +*.log diff --git a/build.gradle b/build.gradle index 3ea1000b5..4b45b49ae 100644 --- a/build.gradle +++ b/build.gradle @@ -1,52 +1,107 @@ +// spring-data-rest - Spring Data REST Exporter apply plugin: "base" -allprojects { +ext { + gradleScriptDir = "${rootProject.projectDir}/gradle" + + // Logging + slf4jVersion = "1.7.2" + logbackVersion = "1.0.7" + + // Spring + springVersion = "3.1.3.RELEASE" + //springVersion = "3.2.0.RELEASE" + hateoasVersion = "0.4.0.BUILD-SNAPSHOT" + springPluginVersion = "0.8.0.BUILD-SNAPSHOT" + springSecurityVersion = "3.1.3.RELEASE" + sdCommonsVersion = "1.5.0.BUILD-SNAPSHOT" + sdJpaVersion = "1.3.0.BUILD-SNAPSHOT" + sdMongoVersion = "1.2.0.BUILD-SNAPSHOT" + sdGemfireVersion = "1.3.0.BUILD-SNAPSHOT" + + // Libraries + guavaVersion = "13.0.1" + jacksonVersion = "2.1.2" + jodaVersion = "2.1" + hibernateVersion = "4.1.7.Final" + hibernateValidatorVersion = "4.3.0.Final" + + // Supporting libraries + cglibVersion = "2.2.2" + + // Testing + junitVersion = "4.11" + hamcrestVersion = "1.3" + jmockVersion = "2.6.0-RC2" + jettyVersion = "8.1.8.v20121106" +} + +buildscript { + repositories { + maven { url "http://repo.springsource.org/plugins-release" } + } + dependencies { + classpath "org.springframework.build.gradle:docbook-reference-plugin:0.2.2" + } +} + +configure(allprojects) { + apply plugin: "java" + apply plugin: "maven" apply plugin: "idea" apply plugin: "eclipse" - apply plugin: "maven" + apply from: "${gradleScriptDir}/ide.gradle" group = "org.springframework.data" - version = "$sdRestVersion" configurations.all { exclude group: "commons-logging" exclude module: "slf4j-log4j12" } + project.sourceCompatibility = 1.6 + project.targetCompatibility = 1.6 + + [compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:none", "-g"] + + sourceSets.test.resources.srcDirs = ["src/test/resources", "src/test/java"] + repositories { - //maven { url "http://repo.springsource.org/libs-snapshot" } - //maven { url "http://repo.springsource.org/libs-milestone" } maven { url "http://repo.springsource.org/libs-release" } + //maven { url "http://repo.springsource.org/libs-milestone" } + maven { url "http://repo.springsource.org/libs-snapshot" } } + dependencies { + // Logging + compile "org.slf4j:slf4j-api:$slf4jVersion" + runtime "org.slf4j:jcl-over-slf4j:$slf4jVersion" + + // Testing + testCompile "junit:junit-dep:$junitVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "org.jmock:jmock-junit4:$jmockVersion" + testCompile "org.jmock:jmock-legacy:$jmockVersion" + testCompile "org.springframework:spring-test:$springVersion" + testRuntime "org.springframework:spring-context-support:$springVersion" + testRuntime "ch.qos.logback:logback-classic:$logbackVersion" + } } configure(subprojects) { subproject -> - apply plugin: "java" - apply plugin: "groovy" - apply from: "${rootProject.projectDir}/maven.gradle" - - configurations { - compile.extendsFrom providedCompile - } - - [compileJava, compileTestJava]*.options*.compilerArgs = ["-Xlint:unchecked"] - - project.sourceCompatibility = 1.6 - project.targetCompatibility = 1.6 + apply from: "${gradleScriptDir}/maven.gradle" javadoc { options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.author = true options.header = subproject.name - //options.overview = "${projectDir}/src/main/java/overview.html" + //options.overview = "${projectDir}/src/api/overview.html" } task sourcesJar(type: Jar, dependsOn: classes) { classifier = "sources" from sourceSets.main.allJava } - task javadocJar(type: Jar) { classifier = "javadoc" from javadoc @@ -56,68 +111,230 @@ configure(subprojects) { subproject -> archives sourcesJar archives javadocJar } +} + +project("spring-data-rest-core") { + description = "Spring Data REST core components." + + configurations { + compile.extendsFrom providedCompile + } 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" + // Google Guava + compile "com.google.guava:guava:$guavaVersion" // Spring - compile("org.springframework:spring-beans:$springVersion") { force = true } - compile("org.springframework:spring-context:$springVersion") { force = true } compile("org.springframework:spring-core:$springVersion") { force = true } - compile("org.springframework:spring-orm:$springVersion") { force = true } - compile("org.springframework:spring-tx:$springVersion") { force = true } compile("org.springframework:spring-web:$springVersion") { force = true } runtime "cglib:cglib-nodep:$cglibVersion" - // Testing - testCompile("org.codehaus.groovy:groovy-all:$groovyVersion") { force = true } - testCompile "org.spockframework:spock-core:$spockVersion" - testCompile "org.spockframework:spock-spring:$spockVersion" - testCompile "org.hamcrest:hamcrest-library:1.3" - testCompile "org.springframework:spring-test:$springVersion" - testRuntime "org.springframework:spring-context-support:$springVersion" - testCompile "org.mockito:mockito-core:1.8.5" - } + // Spring HATEOAS + compile("org.springframework.hateoas:spring-hateoas:$hateoasVersion") { + exclude module: "spring-webmvc" + } + // Spring Data Commons + providedCompile("org.springframework.data:spring-data-commons:$sdCommonsVersion") { + exclude module: "slf4j-api" + exclude module: "jcl-over-slf4j" + } + } +} + +project("spring-data-rest-repository") { + description = "Spring Data REST Repository integration." + + dependencies { + // Exporter core + compile project(":spring-data-rest-core") + + // JSR-305 + compile "com.google.code.findbugs:jsr305:2.0.1" + + // JODA + compile("joda-time:joda-time:$jodaVersion", optional) + + // Jackson JSON + compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + compile("com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion", optional) + compile("com.fasterxml.jackson.datatype:jackson-datatype-hibernate4:$jacksonVersion", optional) + + // ROME + //compile "rome:rome:1.0" + + // Spring + compile("org.springframework:spring-tx:$springVersion") { force = true } + + // JPA + compile("org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final", optional) + + // Spring Plugin + compile "org.springframework.plugin:spring-plugin-core:$springPluginVersion" + + // Spring Data + compile("org.springframework.data:spring-data-jpa:$sdJpaVersion", optional) + compile("org.springframework.data:spring-data-mongodb:$sdMongoVersion", optional) + + // JSR 303 Validation + compile "javax.validation:validation-api:1.0.0.GA" + + // Testing + testCompile "org.hibernate:hibernate-entitymanager:$hibernateVersion" + testCompile "org.hsqldb:hsqldb:2.2.8" + testRuntime "org.hibernate:hibernate-validator:$hibernateValidatorVersion" + } +} + +project("spring-data-rest-webmvc") { + description = "Spring Data REST MVC components." + + dependencies { + // Repository Exporter support + compile project(":spring-data-rest-repository") + + // Spring + compile("org.springframework:spring-webmvc:$springVersion") { force = true } + + // APIS + compile("javax.servlet:javax.servlet-api:3.0.1", provided) + + // Testing + testCompile "org.eclipse.jetty:jetty-servlet:$jettyVersion" + testCompile "org.eclipse.jetty:jetty-webapp:$jettyVersion" + testCompile "org.mozilla:rhino:1.7R4" + } +} + +project("spring-data-rest-example") { + apply plugin: "war" + description = "Spring Data REST example web application." + + dependencies { + compile project(":spring-data-rest-webmvc") + + // Logging + runtime "ch.qos.logback:logback-classic:$logbackVersion" + + // JPA + compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" + + // Spring Data + compile "org.springframework.data:spring-data-jpa:$sdJpaVersion" + compile "org.springframework.data:spring-data-mongodb:$sdMongoVersion" + compile "org.springframework.data:spring-data-gemfire:$sdGemfireVersion" + + // Spring Security + compile "org.springframework.security:spring-security-config:$springSecurityVersion" + compile "org.springframework.security:spring-security-web:$springSecurityVersion" + + // JODA + compile "joda-time:joda-time:$jodaVersion" + + // Jackson + compile "com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion" + compile "com.fasterxml.jackson.datatype:jackson-datatype-hibernate4:$jacksonVersion" + + // Hibernate + runtime "org.hibernate:hibernate-entitymanager:$hibernateVersion" + runtime "org.hibernate:hibernate-validator:$hibernateValidatorVersion" + + // HSQL + runtime "org.hsqldb:hsqldb:2.2.8" + } } configure(rootProject) { + apply plugin: "docbook-reference" - task javadoc(type: Javadoc) { - title = "Spring Data REST ${version} API" - source subprojects.collect { project -> project.sourceSets.main.allJava } - classpath = files(subprojects.collect { project -> project.sourceSets.main.compileClasspath }) - destinationDir = new File(buildDir, "javadoc") + description = "Spring Data REST Exporter" + + reference { + sourceDir = file("src/reference/docbook") + pdfFilename = "spring-data-rest-reference.pdf" + } + + // don"t publish the default jar for the root project + configurations.archives.artifacts.clear() + + task api(type: Javadoc) { + group = "Documentation" + description = "Generates aggregated Javadoc API documentation." + title = "${rootProject.description} ${version} API" options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.author = true - options.header = "Spring Data REST ${version} API" - //options.overview = "${projectDir}/src/main/java/overview.html" + options.header = rootProject.description + options.overview = "src/api/overview.html" + options.splitIndex = true + options.linksOffline "http://docs.oracle.com/javase/6/docs/api/", "http://docs.oracle.com/javase/6/docs/api/" + options.linksOffline "http://static.springsource.org/spring/docs/3.1.x/javadoc-api/", "http://static.springsource.org/spring/docs/3.1.x/javadoc-api/" + options.linksOffline "http://static.springsource.org/spring-data/data-commons/docs/current/api/", "http://static.springsource.org/spring-data/data-commons/docs/current/api/" + + source subprojects.collect { project -> + project.sourceSets.main.allJava + } + + destinationDir = new File(buildDir, "api") + classpath = files(subprojects.collect { project -> + project.sourceSets.main.compileClasspath + }) + maxMemory = "1024m" } -} + task docsZip(type: Zip) { + group = "Distribution" + baseName = "spring-data-rest" + classifier = "docs" + description = "Builds -${classifier} archive containing api and reference " + + "for deployment to http://static.springframework.org/spring-data-rest/docs." -task wrapper(type: Wrapper) { gradleVersion = "1.1" } - -idea { - module { - downloadJavadoc = false - downloadSources = true + from("src/dist") { + include "changelog.txt" + } + from(api) { + into "api" + } + from(reference) { + into "reference" + } } - project { - ipr { - withXml { xml -> - xml.node.component.find { it.@name == "VcsDirectoryMappings" }.mapping.@vcs = "Git" - xml.node.component.find { it.@name == "ProjectRootManager" }.output.@url = "file://\$PROJECT_DIR\$/build" + + dependencies { + // For integration testing + testCompile project(":spring-data-rest-core") + testCompile project(":spring-data-rest-repository") + testCompile project(":spring-data-rest-webmvc") + } + + idea { + module { + downloadJavadoc = false + downloadSources = true + } + project { + ipr { + withXml { xml -> + xml.node.component.find { it.@name == "VcsDirectoryMappings" }.mapping.@vcs = "Git" + xml.node.component.find { it.@name == "ProjectRootManager" }.output.@url = "file://\$PROJECT_DIR\$/build" + } } } } } + +task wrapper(type: Wrapper) { + description = "Generates gradlew[.bat] scripts" + gradleVersion = "1.3" + + doLast() { + def gradleOpts = "-XX:MaxPermSize=1024m -Xmx1024m" + def gradleBatOpts = "$gradleOpts -XX:MaxHeapSize=256m" + File wrapperFile = file("gradlew") + wrapperFile.text = wrapperFile.text.replace("DEFAULT_JVM_OPTS=", + "GRADLE_OPTS=\"$gradleOpts \$GRADLE_OPTS\"\nDEFAULT_JVM_OPTS=") + File wrapperBatFile = file("gradlew.bat") + wrapperBatFile.text = wrapperBatFile.text.replace("set DEFAULT_JVM_OPTS=", + "set GRADLE_OPTS=$gradleBatOpts %GRADLE_OPTS%\nset DEFAULT_JVM_OPTS=") + } +} diff --git a/doc/add_to_existing_app.md b/doc/add_to_existing_app.md deleted file mode 100644 index 242ca598a..000000000 --- a/doc/add_to_existing_app.md +++ /dev/null @@ -1,21 +0,0 @@ -# Adding Spring Data REST to an existing Spring MVC Application - -If you have an existing Spring MVC application and you'd like to integrate Spring Data REST, it's actually very easy. - -Somewhere in your Spring MVC configuration (most likely where you configure your MVC resources) add a bean reference to the JavaConfig class that is responsible for configuring the `RepositoryRestController`. The class name is `org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration`. In XML this would look like: - - - -When your ApplicationContext comes across this bean definition it will bootstrap the necessary Spring MVC resources to fully-configure the controller for exporting the Repositories it finds in that ApplicationContext and any parent contexts. - -### More on required configuration - -There are a couple Spring MVC resources that Spring Data REST depends on that must be configured correctly for it to work inside an existing Spring MVC application. We've tried to isolate those resources from whatever similar resources already exist within your application, but it may be that you want to customize some of the behavior of Spring Data REST by modifying these MVC components. - -The most important things that we configure especially for use by Spring Data REST include: - -#### RepositoryRestHandlerMapping - -We register a custom `HandlerMapping` instance that responds only to the `RepositoryRestController` and only if a path is meant to be handled by Spring Data REST. In order to keep paths that are meant to be handled by your application separate from those handled by Spring Data REST, this custom HandlerMapping inspects the URL path and checks to see if a Repository has been exported under that name. If it has, it allows the request to be handled by Spring Data REST. If there is no Repository exported under that name, it returns `null`, which just means "let other HandlerMapping instances try to service this request". - -Basically this means that Spring Data REST will always be first in line when it comes time to map a URL path and your existing application will never get a chance to service a request that is meant for a Repository. For example, if you have a Repository exported under the name "person", then all requests to your application that start with "/person" will be handled by Spring Data REST and your application will never see that request. If your Repository is exported under a different name, however (like "people"), then requests to "/people" will go to Spring Data REST and requests to "/person" will be handled by your application. \ No newline at end of file diff --git a/doc/changing_json.md b/doc/changing_json.md deleted file mode 100644 index 28d50edf0..000000000 --- a/doc/changing_json.md +++ /dev/null @@ -1,40 +0,0 @@ -# Customizing the JSON output - -Sometimes in your application you need to provide links to other resources from a particular entity. For example, a -`Customer` response might be enriched with links to a current shopping cart, or links to manage resources related to -that entity. Spring Data REST provides integration with [Spring HATEOAS](https://github.com/SpringSource/spring-hateoas) -and provides an extension hook for users to alter the representation of resources going out to the client. - -### The ResourceProcessor interface - -Spring HATEOAS defines a `ResourceProcessor` interface for processing entities. All beans of type -`ResourceProcessor>` will be automatically picked up by the Spring Data REST exporter and triggered when -serializing an entity of type `T`. For example, to define a processor for a `Person` entity, add a `@Bean` to your -ApplicationContext like the following (which is taken from the Spring Data REST tests): - - @Bean public ResourceProcessor> personProcessor() { - return new ResourceProcessor>() { - @Override public Resource process(Resource resource) { - resource.add(new Link("http://localhost:8080/people", "added-link")); - return resource; - } - }; - } - - -### Adding Links - -It's possible to add links to the default representation of an entity by simply calling `resource.add(Link)` like the -example above. Any links you add to the `Resource` will be added to the final output. - -### Customizing the representation - -The Spring Data REST exporter executes any discovered `ResourceProcessor`s before it creates the output representation. -It does this by registering a `Converter` instance with an internal `ConversionService`. This is the -component responsible for creating the links to referenced entities (e.g. those objects under the "links" property in -the object's JSON representation). It takes an `@Entity` and iterates over its properties, creating links for those -properties that are managed by a `Repository` and copying across any embedded or simple properties. - -If your project needs to have output in a different format, however, it's possible to completely replace the default -outgoing JSON representation with your own. If you register your own `ConversionService` in the ApplicationContext and -register your own `Converter`, then you can return a `Resource` implementation of your choosing. diff --git a/doc/configuring_json.md b/doc/configuring_json.md deleted file mode 100644 index c44decac4..000000000 --- a/doc/configuring_json.md +++ /dev/null @@ -1,56 +0,0 @@ -# Adding custom (de)serializers to Jackson's ObjectMapper - -Sometimes the behavior of the Spring Data REST's ObjectMapper, which has been specially configured to use intelligent serializers that can turn domain objects into links and back again, may not handle your domain model correctly. There are so many ways one can structure your data that you may find your own domain model isn't being translated to JSON correctly. It's also sometimes not practical in these cases to try and support a complex domain model in a generic way. Sometimes, depending on the complexity, it's not even possible to offer a generic solution. - -So to accommodate the largest percentage of the use cases, Spring Data REST tries very hard to render your object graph correctly. It will try and serialize unmanaged beans as normal POJOs and it will try and create links to managed beans where that's necessary. But if your domain model doesn't easily lend itself to reading or writing plain JSON, you may want to configure Jackson's ObjectMapper with your own custom type mappings and (de)serializers. - -### Abstract class registration - -One key configuration point you might need to hook into is when you're using an abstract class (or an interface) in your domain model. Jackson won't know by default what implementation to create for an interface. Take the following example: - - @Entity - public class MyEntity { - - @OneToMany - private List interfaces; - - } - -In a default configuration, Jackson has no idea what class to instantiate when POSTing new data to the exporter. This is something you'll need to tell Jackson either through an annotation, or, more cleanly, by registering a type mapping using a [Module](http://wiki.fasterxml.com/JacksonFeatureModules). - -Any `Module` bean declared within the scope of your `ApplicationContext` will be picked up by the exporter and registered with its `ObjectMapper`. To add this special abstract class type mapping, create a `Module` bean and in the `setupModule` method, add an appropriate `TypeResolver`: - - public class MyCustomModule extends SimpleModule { - - private MyCustomModule() { - super("MyCustomModule", new Version(1, 0, 0, "SNAPSHOT")); - } - - @Override public void setupModule(SetupContext context) { - context.addAbstractTypeResolver( - new SimpleAbstractTypeResolver().addMapping(MyInterface.class, MyInterfaceImpl.class) - ); - } - - } - -Once you have access to the `SetupContext` object in your `Module`, you can do all sorts of cool things to configure Jackon's JSON mapping. You can read more about how `Module`s work on Jackson's wiki: [http://wiki.fasterxml.com/JacksonFeatureModules](http://wiki.fasterxml.com/JacksonFeatureModules) - -### Adding custom serializers for domain types - -If you want to (de)serialize a domain type in a special way, you can register your own implementations with Jackson's `ObjectMapper` and the Spring Data REST exporter will transparently handle those domain objects correctly. - -To add serializers, from your `setupModule` method implementation, do something like the following: - - @Override public void setupModule(SetupContext context) { - SimpleSerializers serializers = new SimpleSerializers(); - SimpleDeserializers deserializers = new SimpleDeserializers(); - - serializers.addSerializer(MyEntity.class, new MyEntitySerializer()); - deserializers.addDeserializer(MyEntity.class, mew MyEntityDeserializer()); - - context.addSerializers(serializers); - context.addDeserializers(deserializers); - } - -Now Spring Data REST will correctly handle your domain objects in case they are too complex for the 80% generic use case that Spring Data REST tries to cover. \ No newline at end of file diff --git a/doc/configuring_path.md b/doc/configuring_path.md deleted file mode 100644 index 91b47c95e..000000000 --- a/doc/configuring_path.md +++ /dev/null @@ -1,148 +0,0 @@ -# Configuring the REST URL path - -Configuring the segments of the URL path under which the resources of a JPA Repository are exported is simple. You just add an annotation at the class level and/or at the query method level. - -By default, the exporter will expose your CrudRepository using the class name stripped of the word "Repository". So a Repository defined as follows: - - public interface PersonRepository extends CrudRepository {} - -Will, by default, be exposed under the URL: - - http://localhost:8080/person/ - -To change how the Repository is exported, add a `@RestResource` annotation at the class level: - - @RestResource(path = "people") - public interface PersonRepository extends CrudRepository {} - -Now the Repository will be accessible under the URL: - - http://localhost:8080/people/ - -If you have query methods defined, those also default to be exposed by their name: - - public interface PersonRepository extends CrudRepository { - - public List findByName(String name); - - } - -This would be exposed under the URL: - - http://localhost:8080/person/search/findByName - -_NOTE: All query method resources are exposed under the resource `search`._ - -To change the segment of the URL under which this query method is exposed, use the `@RestResource` annotation again: - - @RestResource(path = "people") - public interface PersonRepository extends CrudRepository { - - @RestResource(path = "names") - public List findByName(String name); - - } - -Now this query method will be exposed under the URL: - - http://localhost:8080/people/search/names - -### Handling rels - -Since these resources are all discoverable, you can also affect how the "rel" attribute is displayed in the links sent out by the exporter. - -For instance, in the default configuration, if you issue a request to `http://localhost:8080/person/search` to find out what query methods are exposed, you'll get back a list of links: - - { - "_links" : [ { - "rel" : "person.findByName", - "href" : "http://localhost:8080/person/search/findByName" - } ] - } - -To change the rel value, use the `rel` property on the `@RestResource` annotation: - - @RestResource(path = "people") - public interface PersonRepository extends CrudRepository { - - @RestResource(path = "names", rel = "names") - public List findByName(String name); - - } - -This would result in a link value of: - - { - "_links" : [ { - "rel" : "person.names", - "href" : "http://localhost:8080/people/search/names" - } ] - } - -The Repository's rel can also be changed by using the `@RestResource` property: - - @RestResource(path = "people", rel = "people") - public interface PersonRepository extends CrudRepository { - - @RestResource(path = "names", rel = "names") - public List findByName(String name); - - } - -This would result in a link value of: - - { - "_links" : [ { - "rel" : "people.names", - "href" : "http://localhost:8080/people/search/names" - } ] - } - -### Hiding certain Repositories, query methods, or fields - -You may not want a certain Repository, a query method on a Repository, or a field of your entity to be exported at all. To tell the exporter to not export these items, annotate them with `@RestResource` and set `exported = false`. - -For example, to skip exporting a Repository: - - @RestResource(exported = false) - public interface PersonRepository extends CrudRepository { - } - -To skip exporting a query method: - - @RestResource(path = "people", rel = "people") - public interface PersonRepository extends CrudRepository { - - @RestResource(exported = false) - public List findByName(String name); - - } - -Or to skip exporting a field: - - @Entity - public class Person { - @Id @GeneratedValue private Long id; - @OneToMany - @RestResource(exported = false) - private Map profiles; - } - -### Hiding Repository CRUD methods - -If you don't want to expose a save or delete method on your `CrudRepository`, you can use the `@RestResource(exported = false)` setting by overriding the method you want to turn off and placing the annotation on the overriden version. For example, to prevent HTTP users from invoking the delete methods of `CrudRepository`, override all of them and add the annotation to the overriden methods. - - @RestResource(path = "people", rel = "people") - public interface PersonRepository extends CrudRepository { - - @Override - @RestResource(exported = false) - void delete(Long id); - - @Override - @RestResource(exported = false) - void delete(Person entity); - - } - -NOTE: It is important that you override _both_ delete methods as the exporter currently uses a somewhat naive algorithm for determing which CRUD method to use in the interest of faster runtime performance. It's not currently possible to turn off the version of delete which takes an ID but leave exported the version that takes an entity instance. For the time being, you can either export the delete methods or not. If you want turn them off, then just keep in mind you have to annotate both versions with `exported = false`. \ No newline at end of file diff --git a/doc/curl_usage.md b/doc/curl_usage.md deleted file mode 100644 index d42a4fc75..000000000 --- a/doc/curl_usage.md +++ /dev/null @@ -1,43 +0,0 @@ -# Example API usage with curl - -Here is some example usage of the REST API with `curl`. First we'll add a `Family`: - - $ curl -v -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family - - HTTP/1.1 201 Created - Location: http://localhost:8080/family/1 - Content-Length: 0 - -Now we'll add a `Person`: - - $ curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people - - HTTP/1.1 201 Created - Location: http://localhost:8080/people/1 - Content-Length: 0 - -Now we'll add this person to the "Doe" family we added above: - - $ curl -v -d 'http://localhost:8080/people/1' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members - - HTTP/1.1 201 Created - Content-Length: 0 - -Notice that we don't return a `Location` when we add items to a referenced collection because we can add N numbers of items (here we're just adding one) so the `Location` header wouldn't be very meaningful as you couldn't match which URL you POSTed with the corresponding URL in the header. - -Now that we have some links created, let's query them so our user agent can keep track of them: - - $ curl -v http://localhost:8080/family/1/members - - HTTP/1.1 200 OK - Content-Type: application/json;charset=ISO-8859-1 - Content-Length: 118 - - { - "_links" : [ { - "rel" : "family.Family.Person.1", - "href" : "http://localhost:8080/family/1/members/1" - } ] - } - -We can continue adding other top-level entities by sending JSON data and can add links to referenced entities by sending `text/uri-list` data with the URIs to those other top-level entities. diff --git a/doc/embedded_entities.md b/doc/embedded_entities.md deleted file mode 100644 index f21b3443c..000000000 --- a/doc/embedded_entities.md +++ /dev/null @@ -1,62 +0,0 @@ -# Embedded Entity references in complex object graphs - -Sometimes it's necessary to populate the incoming JSON with references to pre-existing @Entity objects. Often, this is because of referential integrity constraints. Consider the following relationship between two entities: - - @Entity - public class Person { - // ... person's properties - } - - @Entity - public class Address { - - @OneToOne(optional = false) - private Person person; - - } - -Because of the `optional = false` on the `@OneToOne` annotation, I have to include a reference to an existing `Person` entity if I want to create a new `Address`. - -If you pull up the list of `Person` links using your user agent (Javascript, for instance), you'll want to save the link object that refers to the `Person` instance you're interested in. For example, if I list the `Person`s in the database and use the compact JSON format: - - curl -v -H "Accept: application/x-spring-data-compact+json" http://localhost:8080/people - -I'll get back the link objects I need to reference this entity again (this link is the same as the "self" link that appears in other places): - - { - "links" : [ { - "rel" : "people.Person", - "href" : "http://localhost:8080/people/2" - }, { - "rel" : "people.Person", - "href" : "http://localhost:8080/people/1" - }, { - "rel" : "people.search", - "href" : "http://localhost:8080/people/search" - } ], - "content" : [ ], - "page" : { - "number" : 1, - "size" : 20, - "totalPages" : 1, - "totalElements" : 2 - } - } - -If you present the user with, for example, a drop-down combo box of the `people.Person` type links, and keep track of their selection, then you could just include that object in a new JSON object something like the following: - - { - "postalCode": "12345", - "province": "MO", - "lines": ["1 W 1st St."], - "city": "Univille", - "person": { - "rel" : "people.Person", - "href" : "http://localhost:8080/people/1" - } - } - -You'll remember from the entity definition that the relationship between `Address` and `Person` is not optional. In that case, you'd simply include a JSON link object that refers to the entity you're interested in wherever that entity is supposed to appear. In this case, as the value of the "person" property. You can now POST this JSON to the server to create a new `Address` instance, with the "person" properly populated: - - curl -v -X POST -d '...json data...' http://localhost:8080/address - diff --git a/doc/handling_events.md b/doc/handling_events.md deleted file mode 100644 index 04d52b556..000000000 --- a/doc/handling_events.md +++ /dev/null @@ -1,110 +0,0 @@ -# Handling ApplicationEvents in the REST Exporter - -There are six different events that the REST exporter emits throughout the process of working with an entity. Those are: - -* BeforeSaveEvent -* AfterSaveEvent -* BeforeLinkSaveEvent -* AfterLinkSaveEvent -* BeforeDeleteEvent -* AfterDeleteEvent - -### ApplicationListener - -There is an abstract class you can subclass which listens for these kinds of events and calls -the appropriate method based on the event type. You just override the methods for -the events you're interested in. - - public class BeforeSaveEventListener extends AbstractRepositoryEventListener { - - @Override public void onBeforeSave(Object entity) { - ... logic to handle inspecting the entity before the Repository saves it - } - - @Override public void onAfterDelete(Object entity) { - ... send a message that this entity has been deleted - } - - } - -One thing to note with this approach, however, is that it makes no distinction based on -the type of the entity. You'll have to inspect that yourself. - -### Annotated Handler - -Another approach is to use an annotated handler, which does filter events based on domain type. - -To declare a handler, create a POJO and put the `@RepositoryEventHandler` annotation on it. -This tells the classpath scanner that this class needs to be inspected for handler methods. - -Once it finds a class with this annotation, it iterates over the exposed methods and looks for -annotations that correspond to the event you're interested in. For example, to handle BeforeSaveEvents -in an annotated POJO for different kinds of domain types, you'd define your class like this: - - @RepositoryEventHandler - public class PersonEventHandler { - - @HandleBeforeSave(Person.class) public void handlePersonSave(Person p) { - ... you can now deal with Person in a type-safe way - } - - @HandleBeforeSave(Profile.class) public void handleProfileSave(Profile p) { - ... you can now deal with Profile in a type-safe way - } - - } - -You can also declare the domain type at the class level: - - @RepositoryEventHandler(Person.class) - public class PersonEventHandler { - - @HandleBeforeSave public void handleBeforeSave(Person p) { - ... - } - - @HandleAfterDelete public void handleAfterDelete(Person p) { - ... - } - - } - -To actually get your handler invoked, however, you need to declare an instance of it in your -ApplicationContext. The classpath scanner will look for event handlers and build up information -about them, but it won't actually wire a handler to accept events unless there's an instance of -it declared in your ApplicationContext. - -(In JavaConfig style): - - @Configuration - public class RepositoryConfiguration { - - @Bean PersonEventHandler personEventHandler() { - return new PersonEventHandler(); - } - - } - -When you have your beans properly declared, you need to declare an instance of the ApplicationListener. -You can pass the base package of the packages you want searched for handlers in the constructor. - - @Configuration - public class RepositoryConfiguration { - - @Bean PersonEventHandler personEventHandler() { - return new PersonEventHandler(); - } - - @Bean AnnotatedHandlerRepositoryEventListener repositoryEventListener() { - return new AnnotatedHandlerRepositoryEventListener("com.mycompany.repository.handlers"); - } - - } - -(In XML style): - - - - - - diff --git a/doc/jsonp.md b/doc/jsonp.md deleted file mode 100644 index 945be04d7..000000000 --- a/doc/jsonp.md +++ /dev/null @@ -1,50 +0,0 @@ -# JSONP Support in Spring Data REST - -Spring Data REST supports [JSONP](http://en.wikipedia.org/wiki/JSONP) for doing safe cross-domain Ajax. JSONP support is integrated into the exporter so all you need to do to take advantage of it is pass the appropriate URL parameter. The default parameter is `callback`. So to get query method results wrapped with a call to your Javascript function, add `?callback=my_jsonp_callback` to the URL: - - curl -v http://localhost:8080/people/search/findByName?name=John+Doe&callback=my_json_callback - -Which will result in: - - HTTP/1.1 200 OK - Content-Type: application/javascript - Content-Length: ... - - my_jsonp_callback({ - "results": [ ... ], - "_links": [ ... ] - }) - -### Configuring the URL parameter - -To configure what URL parameter is used, set the `jsonpParamName` property on your `org.springframework.data.rest.webmvc.RepositoryRestConfiguration` bean definition. In JavaConfig this would look like: - - @Bean public RepositoryRestConfiguration restConfig() { - return new RepositoryRestConfiguration(). - setJsonpParamName("jsonp"); - } - -This would mean the above URL would become: - - curl -v http://localhost:8080/people/search/findByName?name=John+Doe&jsonp=my_json_callback - -## JSONP-E Handling Errors - -It's usually not possible to easily handle server errors with JSONP. This is because many JSONP frameworks use a script tag insertion to perform cross-domain Ajax. If the JSONP request results in an HTTP 400 Bad Request, for example, no javascript will be evaluated because the page is considered in error. - -To deftly handle server errors using JSONP, you need to set a value on the `jsonpOnErrParamName` REST exporter configuration property (which is defaulted to `null`, which means don't handle errors). If this value is set, the exception handling code will look for a URL query string parameter of that name and use that javascript function to call as the error handler. The way it does this is by changing the HTTP status code from, for example 400, to 200 (OK). It then wraps the error message with a call to your javascript function and sends the original HTTP status code as the first parameter. - -For example, if a call to POST a new entity results in a validation error, the server will return a 400 Bad Request. If the `jsonpOnErrParamName` is specified and you send that URL parameter, it will instead return a 200 and call your javascript function. Assuming I have `jsonpOnErrParamName` set to "errback", I would trigger this error handling like this: - - curl -v -d '...bad json data...' http://localhost:8080/people?errback=my_jsonp_error_handler - -Which would result in: - - HTTP/1.1 200 OK - Content-Type: application/javascript - Content-Length: ... - - my_jsonp_error_handler(400, { - "message": "Validation failed on property 'name'!", - "cause": { ... } - }) \ No newline at end of file diff --git a/doc/main_wiki.md b/doc/main_wiki.md deleted file mode 100644 index 929538d5b..000000000 --- a/doc/main_wiki.md +++ /dev/null @@ -1,301 +0,0 @@ -# Spring Data JPA Repository Web Exporter - -The Spring Data JPA Repository Web Exporter allows you to export your [JPA Repositories](http://static.springsource.org/spring-data/data-jpa/docs/current/reference/html/#jpa.repositories) as a RESTful web application. The exporter exposes the CRUD methods of a [CrudRepository](http://static.springsource.org/spring-data/data-commons/docs/current/api/org/springframework/data/repository/CrudRepository.html) for doing basic entity management. Relationships can also be managed between linked entities. The exporter is deployed as a traditional Spring MVC Controller, which means all the traditional Spring MVC tools are available to work with the Web Exporter (like Spring Security, for instance). - -### Installation - -#### Servlet environment - -Deployment of the Spring Data Web Exporter is extremely flexible. You can build a WAR file for deploying in a Servlet 2.5 or Servlet 3.0 environment. You can drop the spring-data-rest-webmvc.war artifact into an existing Servlet 3.0 application. - -Start by cloning the base web application project: [https://github.com/SpringSource/spring-data-rest-webmvc](https://github.com/SpringSource/spring-data-rest-webmvc). This sample application contains a `web.xml` file in `src/main/webapp/WEB-INF/servlet-2.5-web.xml` for deployment to pre-servlet-3 containers. The prefered way to configure the exporter, though, is using the XML-free Servlet 3.0 version. Tomcat 7 and Jetty 8 both support deploying this project directly. - - git clone https://github.com/SpringSource/spring-data-rest-webmvc.git - cd spring-data-rest-webmvc - ./gradlew war - -Deploy the built WAR file to your servlet container: - - cp build/libs/spring-data-rest-webmvc-1.0.0.RELEASE.war $TOMCAT_HOME/webapps/data.war - cd $TOMCAT_HOME - bin/catalina.sh run - -You can also run the project directly a Tomcat web container embedded in the build: - - ./gradlew tomcatRun - - The WAR file has a couple example domain classes and exposes a couple repositories by default. You can verify that this configuration is working by issuing an HTTP GET to the root of the web application: - - curl -v http://localhost:8080/spring-data-rest-webmvc/ - - In return, you should see: - - > GET /data/ HTTP/1.1 - > User-Agent: curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8r zlib/1.2.3 - > Host: localhost:8080 - > Accept: */* - > - < HTTP/1.1 200 OK - < Content-Type: application/json;charset=ISO-8859-1 - < Content-Length: 257 - < - { - "links" : [ { - "rel" : "address", - "href" : "http://localhost:8080/spring-data-rest-webmvc/address" - }, { - "rel" : "person", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person" - }, { - "rel" : "profile", - "href" : "http://localhost:8080/spring-data-rest-webmvc/profile" - } ] - } - -### Export Repositories - -The preferred method to configure the Spring Data REST Exporter is to use the JavaConfig annotations. There is an example ApplicationConfig in the example application you can follow. You want to make sure the configuration class with the `@EnableJpaRepositores` annotation on it is loaded by the servlet context loader. Either use the special `RepositoryRestExporterServlet` or a `DispatcherServlet` the the appropriate `contextConfigLocation` set (refer to the `RepositoryRestExporterServlet` for more information). - - @Configuration - @ComponentScan(basePackageClasses = ApplicationConfig.class) - @EnableJpaRepositories - @EnableTransactionManagement - public class ApplicationConfig { - - @Bean public DataSource dataSource() { - EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); - return builder.setType(EmbeddedDatabaseType.HSQL).build(); - } - - @Bean public EntityManagerFactory entityManagerFactory() { - HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); - vendorAdapter.setDatabase(Database.HSQL); - vendorAdapter.setGenerateDdl(true); - - LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); - factory.setJpaVendorAdapter(vendorAdapter); - factory.setPackagesToScan(getClass().getPackage().getName()); - factory.setDataSource(dataSource()); - - factory.afterPropertiesSet(); - - return factory.getObject(); - } - - @Bean public JpaDialect jpaDialect() { - return new HibernateJpaDialect(); - } - - @Bean public PlatformTransactionManager transactionManager() { - JpaTransactionManager txManager = new JpaTransactionManager(); - txManager.setEntityManagerFactory(entityManagerFactory()); - return txManager; - } - - } - -Your `WebApplicationInitializer` class would look like this: - - public class RestExporterWebInitializer implements WebApplicationInitializer { - - @Override public void onStartup(ServletContext ctx) throws ServletException { - - AnnotationConfigWebApplicationContext rootCtx = new AnnotationConfigWebApplicationContext(); - rootCtx.register(ApplicationConfig.class); - - ctx.addListener(new ContextLoaderListener(rootCtx)); - - RepositoryRestExporterServlet exporter = new RepositoryRestExporterServlet(); - - ServletRegistration.Dynamic reg = ctx.addServlet("rest-exporter", exporter); - reg.setLoadOnStartup(1); - reg.addMapping("/*"); - - } - - } - -The REST exporter will also load any XML config files it finds under the path `META-INF/spring-data-rest/*-export.xml`. If you have XML configuration (Spring Integration configuration, for example), then just put your XML files in this location and they will also be bootstrapped in the ApplicationContext. - - - - - - - - - - -### Including your domain artifacts - -To expose your domain objects (your JPA entities, Repositories) and Spring configuration using the web exporter, you need to copy those resources to the web exporter's `WEB-INF/lib` or `WEB-INF/classes` directory. There are potentially other ways to deploy these artifacts without modifying the web exporter's WAR file, but those methods are considerably more complicated and prone to classpath problems. The easiest and most reliable way to deploy your user artifacts are by deploying them alongside the web exporter's artifacts. - -### Exposing your repositories - -By default, any repositories found are exported using the bean name of the repository in the Spring configuration (minus the word "Repository", if it appears in the bean name). - -If you have a JPA entity in your domain model that looks like... - - @Entity - public class Person { - @Id @GeneratedValue - private Long id; - private String name; - @Version - private Long version; - @OneToMany - private List
addresses; - @OneToMany - private Map profiles; - } - -...and an appropriate CrudRepository interface defined like... - - public interface PersonRepository extends CrudRepository { - } - -...your PersonRepository will by default be declared in the ApplicationContext with a bean name of "personRepository". The web exporter will strip the word "Repository" from it and expose a resource named "person". The resulting URL of this repository (assuming the exporter webapp is deployed at context path `/data` in your servlet container) will be `http://localhost:8080/spring-data-rest-webmvc/person`. - -You can configure under what path, or whether a resource is exported at all, by using the `@RestResource` annotation. Details are here: [Configuring the REST URL path](../wiki/Configuring-the-REST-URL-path) - -### Using the rest-shell - -There is a command-line utility to make REST interaction easier. It includes history support and has helper commands to reduce the amount of typing you need to do to effect interactions with your REST services. It's called the `rest-shell`. You can download the binary package or the source from GitHub here: [https://github.com/jbrisbin/rest-shell](https://github.com/jbrisbin/rest-shell). - -### Discoverability - -The Web Exporter implements some aspects of the [HATEOAS](http://en.wikipedia.org/wiki/HATEOAS) methodology. That means all the services of the web exporter are discoverable and exposed to the client using links. - -If you issue an HTTP request to the root of the exporter: - - curl -v http://localhost:8080/spring-data-rest-webmvc/ - -You'll get back a chunk of JSON that points your user agent to the locations of the exported repositories: - - { - "links" : [{ - "rel" : "person", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person" - }] - } - -The "rel" of the link will match the exported name of the repository. Your application should keep track of this rel value as the key to this repository. - -Similarly, if you issue a GET to `http://localhost:8080/spring-data-rest-webmvc/person`, you should get back a list of entities exported at this resource (as returned by the CrudRepository.findAll method). See the wiki for more information about the paging and sorting options. - - curl -v http://localhost:8080/spring-data-rest-webmvc/person - - { - "content": [ ], - "links" : [ { - "rel" : "person.search", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/search" - } ] - } - -The default "rel" of these links will be the rel of the repository plus a dot '.' plus the simple class name of the entity managed by this repository. The rel value can be configured using the `@RestResource` annotation, discussed on [Configuring the REST URL path](../wiki/Configuring-the-REST-URL-path). - -Following these links will give your user agent a chunk of JSON that represents the entity. Besides properly handling nested objects and simple values, the web exporter will show relationships between entities using links just like those presented previously. - - curl -v http://localhost:8080/spring-data-rest-webmvc/person/1 - - { - "name" : "John Doe", - "links" : [ { - "rel" : "profiles", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1/profiles" - }, { - "rel" : "addresses", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1/addresses" - }, { - "rel" : "self", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1" - } ], - "version" : 1 - } - -This entity has a simple String value called "name", and two relationships to other entities ("profiles", and "addresses"). Note that the "rel" value of the link corresponds to the property name of the @Entity. - -The "self" link will always point to the resource for this entity. Use the "self" link to access the entity itself if you wish to update or delete the entity. - -Following the links for the "profiles" property gives us a list of links to the actual entities that are referenced by this relationship: - - curl -v http://localhost:8080/spring-data-rest-webmvc/person/1/profiles - - { - "profiles" : [ { - "rel" : "twitter", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1/profiles/1" - }, { - "rel" : "facebook", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1/profiles/2" - } ] - } - -In this case, the "profiles" property is a Map, so the "rel" value of the links is the key in the Map. The resource link, however, does not use the Map key in the URL. It is consistent with all other links to child resources and uses the ID of the child entity as the last component of the URL. - -Retrieving the linked entity gives us a JSON representation of the entity, as well as the "self" link necessary to update and delete the entity. - - curl -v http://localhost:8080/spring-data-rest-webmvc/person/1/profiles/1 - - { - "links" : [ { - "rel" : "self", - "href" : "http://localhost:8080/spring-data-rest-webmvc/profile/1" - } ], - "type" : "twitter", - "url" : "#!/johndoe" - } - -### Updating relationships - -To maintain a relationship between two entities, access the resource of the relationship by using the id of the entity as the last element in the resource path. For example, to add a link to a Profile with id 3 to a Person with id 1, issue a POST to the "profiles" resource and include in the body of the request a list of resource paths to entities you want to link to (make sure to use the [special Content-Type "text/uri-list"](http://www.ietf.org/rfc/rfc2483.txt) which, as the name implies, is a representation of a list of URIs): - - curl -v -X POST -H "Content-Type: text/uri-list" -d "http://localhost:8080/spring-data-rest-webmvc/profile/3" http://localhost:8080/spring-data-rest-webmvc/person/1/profiles - -You can also delete a relationship by issuing a DELETE request to the resource path that represents the relationship between parent and child entities. For example, to delete a relationship between a Profile entity with an id of 2 and a Person with an id of 1: - - curl -v -X DELETE http://localhost:8080/spring-data-rest-webmvc/person/1/profiles/2 - -### Calling Query methods - -Starting with Spring Data REST 1.0.0.M2, the exporter exposes Repository query methods under the special URL path `/repository/search/*`. - -To see what query methods are exported, issue a GET request to the entity resource URL and add the segment "search". You'll get back a list of links to the exported search methods. - - curl -v http://localhost:8080/spring-data-rest-webmvc/person/search - - { - "links" : [ { - "rel" : "person.findByName", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/search/findByName" - } ] - } - -To query for entities using this search method, add a query parameter to the URL. The response will be a list of links to the top-level URL for that resource. - - curl -v http://localhost:8080/spring-data-rest-webmvc/person/search/findByName?name=John+Doe - - [ { - "rel" : "person.Person", - "href" : "http://localhost:8080/spring-data-rest-webmvc/person/1" - } ] - -To change the URL under which the query method is exported or set the name of the query parameter containing the search term, use the `@RestResource` annotation. - - @RestResource(path = "people") - public interface PersonRepository extends CrudRepository { - - @RestResource(path = "name", rel = "names") - public List findByName(@Param("name") String name); - - } - -This changes the path the PersonRepository is exported under to `/people`, changes the rel of the search URL to `people.names`, changes the path under with the query method is exported to `/name`, and sets the query parameter containing the search term to `name`. To search the Repository using this method, issue a GET request. - - curl -v http://localhost:8080/spring-data-rest-webmvc/people/search/name?name=John+Doe \ No newline at end of file diff --git a/doc/paging_and_sorting.md b/doc/paging_and_sorting.md deleted file mode 100644 index 6c463be22..000000000 --- a/doc/paging_and_sorting.md +++ /dev/null @@ -1,53 +0,0 @@ -# Paging and Sorting - -_This documents Spring Data REST's usage of the Spring Data Repository paging and sorting abstractions. To familiarize yourself with those features, please see the Spring Data documentation for the Repository implementation you're using._ - -## Paging - -Rather than return everything from a large result set, Spring Data REST recognizes some URL parameters that will influence the page size and starting page number. - -To add paging support to your Repositories, you need to extend the `PagingAndSortingRepository` interface rather than the basic `CrudRepository` interface. This adds methods that accept a `Pageable` to control the number and page of results returned. - - public Page findAll(Pageable pageable); - -If you extend `PagingAndSortingRepository` and access the list of all entities, you'll get links to the first 20 entities. To set the page size to any other number, add a `limit` parameter: - - http://localhost:8080/people/?limit=50 - -This will set the page size to 50. - -To use paging in your own query methods, you need to change the method signature to accept an additional `Pageable` parameter and return a `Page` rather than a `List`. For example, the following query method will be exported to `/people/search/nameStartsWith` and will support paging: - - @RestResource(path = "nameStartsWith", rel = "nameStartsWith") - public Page findByNameStartsWith(@Param("name") String name, Pageable p); - -The Spring Data REST exporter will recognize the returned `Page` and give you the results in the body of the response, just as it would with a non-paged response, but additional links will be added to the resource to represent the "previous" and "next" pages of data. - -### Previous and Next Links - -Each paged response will return links to the previous and next pages of results based on the current page. If you are currently at the first page of results, however, no "previous" link will be rendered. The same is true for the last page of results: no "next" link will be rendered if you are on the last page of results. The "rel" value of the link will end with ".next" for next links and ".prev" for previous links. - - { - "rel" : "people.next", - "href" : "http://localhost:8080/people?page=2&limit=20" - } - -### Header metadata when paging - -As a convenience to the user agent, Spring Data REST sets a few special HTTP headers when doing paging. To help the UA understand where it is within the entire set of available pages, three headers are set when returning paged responses: - - x-springdata-meta-total-count: 125 - x-springdata-meta-current-page: 1 - x-springdata-meta-total-pages: 7 - -The UA can use these values to keep track of where the paging stands in relation to the entire result set. This information is useful if you are providing a Javascript slider, for instance. You would be able to easily set the number of "notches" in the slider to the total number of pages and easily indicate to the user exactly where the current page of data falls in the context of the whole. - -## Sorting - -Spring Data REST also recognizes sorting parameters that will use the Repository sorting support. - -To have your results sorted on a particular property, add a `sort` URL parameter with the name of the property you want to sort the results on. You can control the direction of the sort by specifying a URL parameter composed of the property name plus `.dir` and setting that value to either `asc` or `desc`. The following would use the `findByNameStartsWith` query method defined on the `PersonRepository` for all `Person` entities with names starting with the letter "K" and add sort data that orders the results on the `name` property in descending order: - - curl -v http://localhost:8080/people/search/nameStartsWith?name=K&sort=name&name.dir=desc - -To sort the results by more than one property, keep adding as many `sort=PROPERTY` parameters as you need. They will be added to the `Pageable` in the order they appear in the query string. \ No newline at end of file diff --git a/doc/validation.md b/doc/validation.md deleted file mode 100644 index 555b87d36..000000000 --- a/doc/validation.md +++ /dev/null @@ -1,41 +0,0 @@ -# Validation in Spring Data REST - -Integrating validation with the Spring Data REST Exporter is as easy as simply defining an instance of a [Validator](http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/validation/Validator.html). There is an ApplicationListener that that looks for these Validator instances on startup and wires them to the correct RepositoryEvent based on the bean name. - -For example, to validate entities before they are saved to the Repository, you only need to define a Validator instance in your ApplicationContext with a name that starts with "beforeSave". - - - - -All the events dicussed in [Handling ApplicationEvents in the REST Exporter](wiki/Handling-ApplicationEvents-in-the-REST-Exporter) can be validated. - -If any errors are found during validation, a [RepositoryConstraintViolationException](blob/master/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java) will be thrown, resulting in a 400 Bad Request. - -### Advanced Configuration - -If you need a little more control over how the Validators are wired, you can instantiate a [ValidatingRepositoryEventListener](blob/master/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java) yourself and use a Map of Validators to their event names: - - - - - - - - - - - - diff --git a/gradle.properties b/gradle.properties index 4f38d9257..2a49e68a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,26 +1 @@ -# Logging -slf4jVersion = 1.6.6 -logbackVersion = 1.0.6 - -# Libraries -springVersion = 3.1.2.RELEASE -cglibVersion = 2.2.2 - -# Languages -groovyVersion = 1.8.8 - -# Supporting libraries -sdCommonsVersion = 1.4.0.RELEASE -sdJpaVersion = 1.2.0.RELEASE -hateoasVersion = 0.3.0.RELEASE -jacksonVersion = 1.9.10 -hibernateVersion = 4.1.6.Final - -# Testing -spockVersion = 0.6-groovy-1.8 - -## OSGi ranges -spring.range = "[3.0.7, 4.0.0)" -jackson.range = "[1.9, 2.0.0)" - -sdRestVersion = 1.1.0.BUILD-SNAPSHOT +version = 1.1.0.BUILD-SNAPSHOT diff --git a/gradle/ide.gradle b/gradle/ide.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/gradle/maven.gradle b/gradle/maven.gradle new file mode 100644 index 000000000..f2b14fe47 --- /dev/null +++ b/gradle/maven.gradle @@ -0,0 +1,65 @@ +apply plugin: 'maven' + +ext.optionalDeps = [] +ext.providedDeps = [] + +ext.optional = { optionalDeps << it } +ext.provided = { providedDeps << it } + +install { + repositories.mavenInstaller { + customizePom(pom, project) + } +} + +def customizePom(pom, gradleProject) { + pom.whenConfigured { generatedPom -> + // respect 'optional' and 'provided' dependencies + gradleProject.optionalDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.optional = true + } + gradleProject.providedDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.scope = 'provided' + } + + // eliminate test-scoped dependencies (no need in maven central poms) + generatedPom.dependencies.removeAll { dep -> + dep.scope == 'test' + } + + // add all items necessary for maven central publication + generatedPom.project { + name = "Spring Data REST" + description = "Directly export Spring Data-managed PersistentEntities to the web." + url = 'http://github.com/SpringSource/spring-data-rest' + organization { + name = 'SpringSource' + url = 'http://www.springsource.org/spring-data/rest' + } + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + url = 'http://github.com/SpringSource/spring-data-rest' + connection = 'scm:git:git://github.com/SpringSource/spring-data-rest' + developerConnection = 'scm:git:git://github.com/SpringSource/spring-data-rest' + } + developers { + developer { + id = 'jbrisbin' + name = 'Jon Brisbin' + email = 'jbrisbin@vmware.com' + } + developer { + id = 'ogierke' + name = 'Oliver Gierke' + email = 'ogierke@vmware.com' + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f1e239c8..b6b646b0c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7b8984830..dfaae9ee9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Aug 15 14:17:51 CDT 2012 +#Wed Dec 05 08:31:21 CST 2012 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.1-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.3-bin.zip diff --git a/gradlew b/gradlew index e61422d06..a1787c628 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ############################################################################## ## @@ -7,6 +7,7 @@ ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +GRADLE_OPTS="-XX:MaxPermSize=1024m -Xmx1024m $GRADLE_OPTS" DEFAULT_JVM_OPTS="" APP_NAME="Gradle" @@ -61,9 +62,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" +cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" -cd "$SAVED" +cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar diff --git a/gradlew.bat b/gradlew.bat index aec99730b..e6e6d5ce3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,91 @@ -@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 +@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 GRADLE_OPTS=-XX:MaxPermSize=1024m -Xmx1024m -XX:MaxHeapSize=256m %GRADLE_OPTS% +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/maven.gradle b/maven.gradle deleted file mode 100644 index 399dc0c8e..000000000 --- a/maven.gradle +++ /dev/null @@ -1,60 +0,0 @@ -apply plugin: 'maven' - -ext.optionalDeps = [] -ext.providedDeps = [] - -ext.optional = { optionalDeps << it } -ext.provided = { providedDeps << it } - -install { - repositories.mavenInstaller { - customizePom(pom, project) - } -} - -def customizePom(pom, gradleProject) { - pom.whenConfigured { generatedPom -> - // respect 'optional' and 'provided' dependencies - gradleProject.optionalDeps.each { dep -> - generatedPom.dependencies.find { it.artifactId == dep.name }?.optional = true - } - gradleProject.providedDeps.each { dep -> - generatedPom.dependencies.find { it.artifactId == dep.name }?.scope = 'provided' - } - - // eliminate test-scoped dependencies (no need in maven central poms) - generatedPom.dependencies.removeAll { dep -> - dep.scope == 'test' - } - - // add all items necessary for maven central publication - generatedPom.project { - name = "Spring Data REST" - description = "Directly export Spring Data managed JPA Entities to the web." - url = 'http://github.com/SpringSource/spring-data-rest' - organization { - name = 'SpringSource' - url = 'http://www.springsource.org/spring-data/rest' - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - scm { - url = 'http://github.com/SpringSource/spring-data-rest' - connection = 'scm:git:git://github.com/SpringSource/spring-data-rest' - developerConnection = 'scm:git:git://github.com/SpringSource/spring-data-rest' - } - developers { - developer { - id = 'jbrisbin' - name = 'Jon Brisbin' - email = 'jbrisbin@vmware.com' - } - } - } - } -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a030b0942..a48202765 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,6 @@ +rootProject.name = "spring-data-rest" + include "spring-data-rest-core", "spring-data-rest-repository", - "spring-data-rest-webmvc" + "spring-data-rest-webmvc", + "spring-data-rest-example" \ No newline at end of file diff --git a/spring-data-rest-core/build.gradle b/spring-data-rest-core/build.gradle deleted file mode 100644 index a222d5dbf..000000000 --- a/spring-data-rest-core/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -dependencies { - - // Google Guava - compile "com.google.guava:guava:12.0" - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/DelegatingConversionService.java similarity index 86% rename from spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java rename to spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/DelegatingConversionService.java index 83c2c20bc..b3ec428cd 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/DelegatingConversionService.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.core.convert; +package org.springframework.data.rest.convert; import java.util.Stack; @@ -7,14 +7,13 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; /** - * This {@link ConversionService} implementation delegates the actual conversion the ConversionService if finds in its - * internal List that claims to be able to convert a given class. It will roll through the internal Stack - * of ConversionServices until it finds one that can convert the given type. + * This {@link ConversionService} implementation delegates the actual conversion to the {@literal ConversionService} it + * finds in its internal {@link Stack} that claims to be able to convert a given class. It will roll through the + * {@literal ConversionService}s until it finds one that can convert the given type. * * @author Jon Brisbin */ -public class DelegatingConversionService - implements ConversionService { +public class DelegatingConversionService implements ConversionService { private Stack conversionServices = new Stack(); diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/ISO8601DateConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/ISO8601DateConverter.java new file mode 100644 index 000000000..98a547003 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/ISO8601DateConverter.java @@ -0,0 +1,76 @@ +package org.springframework.data.rest.convert; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; + +/** + * @author Jon Brisbin + */ +public class ISO8601DateConverter implements ConditionalGenericConverter, + Converter { + + public static final ConditionalGenericConverter INSTANCE = new ISO8601DateConverter(); + + private static final Set CONVERTIBLE_PAIRS = new HashSet(); + + static { + CONVERTIBLE_PAIRS.add(new ConvertiblePair(String.class, Date.class)); + CONVERTIBLE_PAIRS.add(new ConvertiblePair(Date.class, String.class)); + } + + @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if(String.class.isAssignableFrom(sourceType.getType())) { + return Date.class.isAssignableFrom(targetType.getType()); + } + + return Date.class.isAssignableFrom(sourceType.getType()) + && String.class.isAssignableFrom(targetType.getType()); + } + + @Override public Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + DateFormat dateFmt = iso8601DateFormat(); + if(String.class.isAssignableFrom(sourceType.getType())) { + return dateFmt.format(source); + } else { + try { + return dateFmt.parse(source.toString()); + } catch(ParseException e) { + throw new ConversionFailedException(sourceType, targetType, source, e); + } + } + } + + @Override public Date convert(String[] source) { + if(source.length > 0) { + try { + return iso8601DateFormat().parse(source[0]); + } catch(ParseException e) { + throw new ConversionFailedException( + TypeDescriptor.valueOf(String[].class), + TypeDescriptor.valueOf(Date.class), + source[0], + new IllegalArgumentException("Source does not conform to ISO8601 date format (YYYY-MM-DDTHH:MM:SS-0000") + ); + } + } + return null; + } + + private DateFormat iso8601DateFormat() { + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/UUIDConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/UUIDConverter.java new file mode 100644 index 000000000..5b2ccf31a --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/UUIDConverter.java @@ -0,0 +1,46 @@ +package org.springframework.data.rest.convert; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * For converting a {@link UUID} into a {@link String}. + * + * @author Jon Brisbin + */ +public class UUIDConverter implements ConditionalGenericConverter { + + public static final UUIDConverter INSTANCE = new UUIDConverter(); + private static final Set CONVERTIBLE_PAIRS = new HashSet(); + + static { + CONVERTIBLE_PAIRS.add(new ConvertiblePair(String.class, UUID.class)); + CONVERTIBLE_PAIRS.add(new ConvertiblePair(UUID.class, String.class)); + } + + @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if(String.class.isAssignableFrom(sourceType.getType())) { + return UUID.class.isAssignableFrom(targetType.getType()); + } + + return UUID.class.isAssignableFrom(sourceType.getType()) + && String.class.isAssignableFrom(targetType.getType()); + } + + @Override public Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if(String.class.isAssignableFrom(sourceType.getType())) { + return UUID.fromString(source.toString()); + } else { + return source.toString(); + } + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/package-info.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/package-info.java new file mode 100644 index 000000000..2a539f0cc --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/convert/package-info.java @@ -0,0 +1,4 @@ +/** + * {@link org.springframework.core.convert.ConversionService} and {@link org.springframework.core.convert.converter.Converter} integration for Spring Data REST. + */ +package org.springframework.data.rest.convert; diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java deleted file mode 100644 index 2d4e0566d..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.springframework.data.rest.core; - -/** - * Generic interface used as a callback in any place you need extensibility. - * - * @author Jon Brisbin - */ -public interface Handler { - - /** - * Accept an argument and possibly produce a result. - * - * @param t - * arg - * - * @return Some object or {@literal null} if no result. - */ - V handle(T t); - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriResolver.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriResolver.java deleted file mode 100644 index 31500475f..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriResolver.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.springframework.data.rest.core; - -import java.net.URI; - -/** - * Implementations of this interface are responsible for turning {@link URI}s into real objects. - * - * @author Jon Brisbin - */ -public interface UriResolver { - - /** - * Take a {@link URI} and resolve it to an actual object. - * - * @param baseUri - * The base URI that this resource is relative to. - * @param uri - * The URI id of the resource. - * - * @return The resolved object or {@literal null} if not found. - */ - T resolve(URI baseUri, URI uri); - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java deleted file mode 100644 index d90c91d5f..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.springframework.data.rest.core.convert; - -import java.util.UUID; - -import org.springframework.core.convert.converter.Converter; - -/** - * @author Jon Brisbin - */ -public class StringToUUIDConverter - implements Converter { - @Override public UUID convert(String s) { - return (null != s ? UUID.fromString(s) : null); - } -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java deleted file mode 100644 index 51df846ab..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.springframework.data.rest.core.convert; - -import java.util.UUID; - -import org.springframework.core.convert.converter.Converter; - -/** - * @author Jon Brisbin - */ -public class UUIDToStringConverter - implements Converter { - @Override public String convert(UUID uuid) { - return (null != uuid ? uuid.toString() : null); - } -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/package-info.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/package-info.java new file mode 100644 index 000000000..ba9ee30c4 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/package-info.java @@ -0,0 +1,4 @@ +/** + * Core components used across Spring Data REST. + */ +package org.springframework.data.rest.core; diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java deleted file mode 100644 index d3d1149a4..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.springframework.data.rest.core.util; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.util.concurrent.UncheckedExecutionException; -import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * @author Jon Brisbin - */ -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/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java deleted file mode 100644 index 541c270c2..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.springframework.data.rest.core.util; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import org.springframework.util.ReflectionUtils; - -/** - * Helper methods for dealing with the metadata of "fluent" beans. - * - * @author Jon Brisbin - */ -public abstract class FluentBeanUtils { - - private static final LoadingCache, 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)) { - ReflectionUtils.makeAccessible(method); - if(method.getParameterTypes().length == 0) { - meta.getters.put(fname, method); - } else if(method.getParameterTypes().length == 1) { - meta.setters.put(fname, method); - } - meta.fieldNames.add(fname); - } - } - }); - ReflectionUtils.makeAccessible(field); - meta.fields.put(fname, field); - } - } - } - ); - return meta; - } - } - ); - - /** - * Interrogate a bean and collect {@link Metadata} on it. - * - * @param targetType - * The type to interrogate. - * - * @return {@link Metadata} for the fluent bean. - */ - public static Metadata metadata(Class targetType) { - try { - return metadata.get(targetType); - } catch(ExecutionException e) { - throw new IllegalStateException(e); - } - } - - /** - * Set the property of a fluent bean. - * - * @param property - * Name of the property to set. - * @param value - * Value of the property. - * @param bean - * Bean on which to set this property. - * - * @return Usually {@literal null} but will return whatever the "setter" returns, which could be {@this} or something - * else. - */ - public static Object set(String property, Object value, Object bean) { - if(null == bean) { - return null; - } - - Class type = bean.getClass(); - try { - Method setter = metadata.get(type).setters.get(property); - if(null != setter) { - return setter.invoke(bean, value); - } - - Field f = metadata.get(type).fields.get(property); - if(null == f) { - return null; - } - - f.set(bean, value); - - return bean; - } catch(Throwable t) { - throw new IllegalArgumentException(t.getMessage(), t); - } - } - - /** - * Get the value of a property. - * - * @param property - * Name of the property. - * @param bean - * Bean of which to get the property. - * - * @return Value of the property. Could be {@literal null} - */ - public static Object get(String property, Object bean) { - if(null == bean) { - return null; - } - - Class type = bean.getClass(); - try { - Method getter = metadata.get(type).getters.get(property); - if(null != getter) { - return getter.invoke(bean); - } - - Field f = metadata.get(type).fields.get(property); - if(null == f) { - return null; - } - - return f.get(bean); - } catch(Throwable t) { - throw new IllegalStateException(t.getMessage(), t); - } - } - - /** - * Determines whether a given type looks like a fluent bean. That means it has methods whose names exactly correspond - * to a field of the same name. A "getter" is that method which is named the same as the field and has 0 parameters. - * The "setter" is that method which is named the same as the field and has a single argument. - * - * @param type - * The class to inspect. - * - * @return {@literal true} if this looks like a fluent bean, {@literal false} otherwise. - */ - public static boolean isFluentBean(Class type) { - try { - return metadata.get(type).getters.size() > 0; - } catch(ExecutionException e) { - throw new IllegalStateException(e); - } - } - - public static class Metadata { - List fieldNames = new ArrayList(); - Map fields = new HashMap(); - Map getters = new HashMap(); - Map setters = new HashMap(); - - public List fieldNames() { - return fieldNames; - } - - public Map getters() { - return getters; - } - - public Map setters() { - return setters; - } - - public Map fields() { - return fields; - } - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java deleted file mode 100644 index c74b2304f..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/RestHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.springframework.data.rest.core.util; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -/** - * @author Jon Brisbin - */ -public class RestHelper { - - public HttpStatus status; - public HttpHeaders headers = new HttpHeaders(); - public T body; - - private RestHelper(T body) { - this.body = body; - } - - public static RestHelper resource(T body) { - return new RestHelper(body); - } - - public RestHelper header(String key, String value) { - headers.add(key, value); - return this; - } - - public RestHelper status(HttpStatus status) { - this.status = status; - return this; - } - - public HttpEntity asHttpEntity() { - return new HttpEntity(body, headers); - } - - public ResponseEntity asResponseEntity() { - return new ResponseEntity(body, headers, status); - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java index a09439572..e07d32c90 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java @@ -4,7 +4,7 @@ import java.net.URI; import java.util.List; import java.util.Stack; -import org.springframework.data.rest.core.Handler; +import com.google.common.base.Function; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; @@ -37,7 +37,7 @@ public abstract class UriUtils { } /** - * Execute the given {@link Handler} for each segment in the {@link URI}. + * Execute the given {@link Function} for each segment in the {@link URI}. *

e.g. given a URI of {@literal http://localhost:8080/data/person/1} and a base URI of {@code * http://localhost:8080/data}, this method will explode the URI into it's components, as compared to the base URI. * The result would be: the given handler gets called twice, once passing a relative {@link URI} of "person" and a @@ -47,19 +47,19 @@ public abstract class UriUtils { * @param baseUri * base {@link URI} * @param uri - * {@link URI} to explode and iteratre over. + * {@link URI} to explode and iterate over. * @param handler - * {@link Handler} to call for each segment of the URI's path. + * {@link Function} to call for each segment of the URI's path. * @param * Return type of the handler. * * @return Handler return value, or possibly {@literal null}. */ - public static V foreach(URI baseUri, URI uri, Handler handler) { + public static V foreach(URI baseUri, URI uri, Function handler) { List uris = explode(baseUri, uri); V v = null; for(URI u : uris) { - v = handler.handle(u); + v = handler.apply(u); } return v; } @@ -163,8 +163,9 @@ public abstract class UriUtils { * Just the path portion of the {@link URI}, but with any trailing slash "/" removed. * * @param uri + * path URI * - * @return + * @return the path portion of the URI, but with any trailing slash removed */ public static String path(URI uri) { if(null == uri) { @@ -197,9 +198,11 @@ public abstract class UriUtils { * Create a new {@link URI} out of the components. * * @param baseUri + * The base URI these path segments are relative to. * @param pathSegments + * The path segments to add to the given base URI. * - * @return + * @return A new URI built from the given base URI and additional path segments. */ public static URI buildUri(URI baseUri, String... pathSegments) { return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); diff --git a/spring-data-rest-core/src/test/groovy/org/springframework/data/rest/core/spec/UriUtilsSpec.groovy b/spring-data-rest-core/src/test/groovy/org/springframework/data/rest/core/spec/UriUtilsSpec.groovy deleted file mode 100644 index 578081862..000000000 --- a/spring-data-rest-core/src/test/groovy/org/springframework/data/rest/core/spec/UriUtilsSpec.groovy +++ /dev/null @@ -1,48 +0,0 @@ -package org.springframework.data.rest.core.spec - -import org.springframework.data.rest.core.util.UriUtils -import spock.lang.Specification - -/** - * @author Jon Brisbin - */ -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/spring-data-rest-core/src/test/java/org/springframework/data/rest/AbstractJMockTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/AbstractJMockTests.java new file mode 100644 index 000000000..1e87fc0da --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/AbstractJMockTests.java @@ -0,0 +1,20 @@ +package org.springframework.data.rest; + +import org.jmock.integration.junit4.JMock; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.jmock.lib.legacy.ClassImposteriser; +import org.junit.runner.RunWith; + +/** + * Abstract base classes for JUnit tests that use JMock. + * + * @author Jon Brisbin + */ +@RunWith(JMock.class) +public abstract class AbstractJMockTests { + + protected JUnit4Mockery context = new JUnit4Mockery() {{ + setImposteriser(ClassImposteriser.INSTANCE); + }}; + +} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/convert/DelegatingConversionServiceUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/convert/DelegatingConversionServiceUnitTests.java new file mode 100644 index 000000000..3da9fd1e3 --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/convert/DelegatingConversionServiceUnitTests.java @@ -0,0 +1,64 @@ +package org.springframework.data.rest.convert; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.util.UUID; + +import org.jmock.Expectations; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.rest.AbstractJMockTests; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * Tests to ensure the {@link DelegatingConversionService} properly delegates conversions to the {@link + * org.springframework.core.convert.ConversionService} that is appropriate for the given source and return types. + * + * @author Jon Brisbin + */ +public class DelegatingConversionServiceUnitTests extends AbstractJMockTests { + + private static final UUID RANDOM_UUID = UUID.fromString("9deccfd7-f892-4e26-a4d5-c92893392e78"); + + private ConversionService conversionService; + private DelegatingConversionService delegatingConversionService; + + @Before + public void setup() { + conversionService = context.mock(ConversionService.class); + + DefaultFormattingConversionService cs = new DefaultFormattingConversionService(false); + cs.addConverter(UUIDConverter.INSTANCE); + + delegatingConversionService = new DelegatingConversionService( + conversionService, + cs + ); + + context.checking(new Expectations() {{ + allowing(conversionService).canConvert(String.class, UUID.class); + will(returnValue(false)); + allowing(conversionService).canConvert(UUID.class, String.class); + will(returnValue(false)); + + // Ensure the first ConversionService is never asked to convert this String into a UUID + never(conversionService).convert(with(any(String.class)), with(UUID.class)); + never(conversionService).convert(with(any(UUID.class)), with(String.class)); + }}); + } + + @Test + public void shouldDelegateToProperConversionService() throws Exception { + assertThat(delegatingConversionService.canConvert(String.class, UUID.class), is(true)); + assertThat(delegatingConversionService.convert(RANDOM_UUID.toString(), UUID.class), is(RANDOM_UUID)); + } + + @Test + public void shouldConvertUUIDToString() throws Exception { + assertThat(delegatingConversionService.canConvert(UUID.class, String.class), is(true)); + assertThat(delegatingConversionService.convert(RANDOM_UUID, String.class), is(RANDOM_UUID.toString())); + } + +} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java new file mode 100644 index 000000000..98c7d018e --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java @@ -0,0 +1,93 @@ +package org.springframework.data.rest.core.util; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import com.google.common.base.Function; +import org.junit.Test; + +/** + * Tests to verify that {@link UriUtils} can manipulate {@link URI}s. + * + * @author Jon Brisbin + */ +public class UriUtilsUnitTests { + + private static final String BASE_URI_STR = "http://localhost:8080/data"; + private static final URI BASE_URI = URI.create(BASE_URI_STR); + + private static final String PERSON_2LVL_STR = BASE_URI_STR + "/person/1"; + private static final URI PERSON_2LVL_URI = URI.create(PERSON_2LVL_STR); + + @Test + public void shouldValidateBaseURI() throws Exception { + URI uri = new URI(BASE_URI + "/person/1"); + + assertThat(UriUtils.validBaseUri(BASE_URI, uri), is(true)); + } + + @Test + public void shouldIterateOverPathElements() throws Exception { + final List paths = new ArrayList(); + Function fn = new Function() { + @Override public Void apply(URI uri) { + paths.add(uri.getPath()); + return null; + } + }; + + UriUtils.foreach(BASE_URI, PERSON_2LVL_URI, fn); + + assertThat(paths, hasSize(2)); + assertThat(paths, contains("person", "1")); + } + + @Test + public void shouldExplodeRelativeURI() throws Exception { + Stack uris = UriUtils.explode(BASE_URI, PERSON_2LVL_URI); + + assertThat(uris, hasSize(2)); + assertThat(uris, contains(URI.create("person"), URI.create("1"))); + } + + @Test + public void shouldMergeDifferentURIsIntoOne() throws Exception { + String qrystr = "?queryParam=testValue"; + + URI uriWithQuery = URI.create(qrystr); + URI uriWithPath = URI.create("person/1"); + + URI uri = UriUtils.merge(BASE_URI, uriWithPath, uriWithQuery); + + assertThat(uri.toString(), is(PERSON_2LVL_STR + qrystr)); + } + + @Test + public void shouldStripTrailingSlashFromPath() throws Exception { + URI uri = URI.create("person/"); + + String path = UriUtils.path(uri); + + assertThat(path, is("person")); + } + + @Test + public void shouldStripTheLastPathSegmentFromAURI() throws Exception { + URI uri = UriUtils.tail(BASE_URI, PERSON_2LVL_URI); + + assertThat(uri, is(URI.create("1"))); + } + + @Test + public void shouldBuildURIFromPathSegments() throws Exception { + URI uri = UriUtils.buildUri(BASE_URI, "person", "1"); + + assertThat(uri, is(PERSON_2LVL_URI)); + } + +} diff --git a/spring-data-rest-core/src/test/resources/logback.xml b/spring-data-rest-core/src/test/resources/logback.xml index 919ca3f93..905c91963 100644 --- a/spring-data-rest-core/src/test/resources/logback.xml +++ b/spring-data-rest-core/src/test/resources/logback.xml @@ -8,8 +8,7 @@ - - + diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleConfig.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleConfig.java new file mode 100644 index 000000000..d33df8dca --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleConfig.java @@ -0,0 +1,13 @@ +package org.springframework.data.rest.example; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +/** + * @author Jon Brisbin + */ +@Configuration +@ImportResource("classpath:") +public class RestExporterExampleConfig { + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleRestConfig.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleRestConfig.java new file mode 100644 index 000000000..e4e43dbf8 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterExampleRestConfig.java @@ -0,0 +1,35 @@ +package org.springframework.data.rest.example; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.example.jpa.Person; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; + +/** + * @author Jon Brisbin + */ +@Configuration +@ImportResource("classpath:META-INF/spring/security-config.xml") +public class RestExporterExampleRestConfig extends RepositoryRestMvcConfiguration { + + @Override protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { + config.addResourceMappingForDomainType(Person.class) + .addResourceMappingFor("lastName") + .setPath("surname"); + config.addResourceMappingForDomainType(Person.class) + .addResourceMappingFor("siblings") + .setRel("siblings") + .setPath("siblings"); + } + + // @Bean public ResourceProcessor> globalResourceProcessor() { + // return new ResourceProcessor>() { + // @Override public Resource process(Resource resource) { + // resource.add(new Link("href", "rel")); + // return resource; + // } + // }; + // } + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterWebInitializer.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterWebInitializer.java new file mode 100644 index 000000000..f5584b8a3 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/RestExporterWebInitializer.java @@ -0,0 +1,49 @@ +package org.springframework.data.rest.example; + +import java.util.EnumSet; +import javax.servlet.DispatcherType; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; + +import org.springframework.data.rest.example.gemfire.GemfireRepositoryConfig; +import org.springframework.data.rest.example.jpa.JpaRepositoryConfig; +import org.springframework.data.rest.example.mongodb.MongoDbRepositoryConfig; +import org.springframework.data.rest.webmvc.RepositoryRestDispatcherServlet; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; + +/** + * @author Jon Brisbin + */ +public class RestExporterWebInitializer implements WebApplicationInitializer { + + @Override public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext rootCtx = new AnnotationConfigWebApplicationContext(); + rootCtx.register( + JpaRepositoryConfig.class, + MongoDbRepositoryConfig.class, + GemfireRepositoryConfig.class + ); + + servletContext.addListener(new ContextLoaderListener(rootCtx)); + servletContext.addFilter("springSecurity", DelegatingFilterProxy.class); + servletContext.getFilterRegistration("springSecurity").addMappingForUrlPatterns( + EnumSet.of(DispatcherType.REQUEST), + false, + "/*" + ); + + AnnotationConfigWebApplicationContext webCtx = new AnnotationConfigWebApplicationContext(); + webCtx.register(RestExporterExampleRestConfig.class); + + RepositoryRestDispatcherServlet dispatcherServlet = new RepositoryRestDispatcherServlet(webCtx); + ServletRegistration.Dynamic reg = servletContext.addServlet("rest-exporter", dispatcherServlet); + reg.setLoadOnStartup(1); + reg.addMapping("/*"); + + } + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/GemfireRepositoryConfig.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/GemfireRepositoryConfig.java new file mode 100644 index 000000000..0f9e6d626 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/GemfireRepositoryConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; + +/** + * Spring JavaConfig configuration class to setup a Spring container and infrastructure components. + * + * @author Oliver Gierke + * @author David Turanski + */ +@Configuration +@ImportResource("classpath:META-INF/spring/cache-config.xml") +@EnableGemfireRepositories +public class GemfireRepositoryConfig { + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/AbstractPersistentEntity.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/AbstractPersistentEntity.java new file mode 100644 index 000000000..8c9cc7565 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/AbstractPersistentEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import org.springframework.data.annotation.Id; + +/** + * Base class for persistent classes. + * + * @author Oliver Gierke + * @author David Turanski + */ +public class AbstractPersistentEntity { + + @Id + private final Long id; + + /** + * Returns the identifier of the entity. + * + * @return the id + */ + public Long getId() { + return id; + } + + protected AbstractPersistentEntity(Long id) { + this.id = id; + } + + protected AbstractPersistentEntity() { + this.id = null; + } + + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (this.id == null || obj == null || !(this.getClass().equals(obj.getClass()))) { + return false; + } + + AbstractPersistentEntity that = (AbstractPersistentEntity) obj; + + return this.id.equals(that.getId()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return id == null ? 0 : id.hashCode(); + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Address.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Address.java new file mode 100644 index 000000000..5b258ab5b --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Address.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import org.springframework.util.Assert; + +/** + * An address. + * + * @author Oliver Gierke + */ +public class Address { + + private final String street, city, country; + + /** + * Creates a new {@link Address} from the given street, city and country. + * + * @param street must not be {@literal null} or empty. + * @param city must not be {@literal null} or empty. + * @param country must not be {@literal null} or empty. + */ + public Address(String street, String city, String country) { + + Assert.hasText(street, "Street must not be null or empty!"); + Assert.hasText(city, "City must not be null or empty!"); + Assert.hasText(country, "Country must not be null or empty!"); + + this.street = street; + this.city = city; + this.country = country; + } + + /** + * Returns a copy of the current {@link Address} instance which is a new entity in terms of persistence. + * + * @return + */ + public Address getCopy() { + return new Address(this.street, this.city, this.country); + } + + /** + * Returns the street. + * + * @return + */ + public String getStreet() { + return street; + } + + /** + * Returns the city. + * + * @return + */ + public String getCity() { + return city; + } + + /** + * Returns the country. + * + * @return + */ + public String getCountry() { + return country; + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Customer.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Customer.java new file mode 100644 index 000000000..53fc172ea --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Customer.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.data.gemfire.mapping.Region; +import org.springframework.util.Assert; + + +/** + * A customer. + * + * @author Oliver Gierke + * @author David Turanski + */ +@Region +public class Customer extends AbstractPersistentEntity { + private EmailAddress emailAddress; + private String firstname, lastname; + private Set

addresses = new HashSet
(); + + /** + * Creates a new {@link Customer} from the given parameters. + * + * @param id + * the unique id; + * @param emailAddress + * must not be {@literal null} or empty. + * @param firstname + * must not be {@literal null} or empty. + * @param lastname + * must not be {@literal null} or empty. + */ + public Customer(Long id, EmailAddress emailAddress, String firstname, String lastname) { + super(id); + Assert.hasText(firstname); + Assert.hasText(lastname); + Assert.notNull(emailAddress); + + this.firstname = firstname; + this.lastname = lastname; + this.emailAddress = emailAddress; + } + + protected Customer() { + } + + /** + * Adds the given {@link Address} to the {@link Customer}. + * + * @param address + * must not be {@literal null}. + */ + public void add(Address address) { + + Assert.notNull(address); + this.addresses.add(address); + } + + /** + * Returns the firstname of the {@link Customer}. + * + * @return + */ + public String getFirstname() { + return firstname; + } + + /** + * Sets the firstname of the {@link Customer}. + * + * @param firstname + */ + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + /** + * Returns the lastname of the {@link Customer}. + * + * @return + */ + public String getLastname() { + return lastname; + } + + /** + * Sets the lastname of the {@link Customer}. + * + * @param lastname + */ + public void setLastname(String lastname) { + this.lastname = lastname; + } + + /** + * Returns the {@link EmailAddress} of the {@link Customer}. + * + * @return + */ + public EmailAddress getEmailAddress() { + return emailAddress; + } + + /** + * Sets the emailAddress of the {@link Customer}. + * + * @param emailAddress + */ + public void setEmailAddress(EmailAddress emailAddress) { + this.emailAddress = emailAddress; + } + + /** + * Return the {@link Customer}'s addresses. + * + * @return + */ + public Set
getAddresses() { + return Collections.unmodifiableSet(addresses); + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/CustomerRepository.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/CustomerRepository.java new file mode 100644 index 000000000..4e4631759 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/CustomerRepository.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +/** + * Repository interface to access {@link Customer}s. + * + * @author Oliver Gierke + * @author David Turanski + */ + +public interface CustomerRepository extends CrudRepository { + + /** + * Finds all {@link Customer}s with the given lastname. + * + * @param lastname + * + * @return + */ + List findByLastname(@Param("lastname") String lastname); + + /** + * Finds the Customer with the given {@link EmailAddress}. + * + * @param emailAddress + * + * @return + */ + Customer findByEmailAddress(@Param("email") EmailAddress emailAddress); + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/EmailAddress.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/EmailAddress.java new file mode 100644 index 000000000..90ae8333f --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/EmailAddress.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import java.util.regex.Pattern; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Value object to represent email addresses. + * + * @author Oliver Gierke + */ +@JsonSerialize(using = ToStringSerializer.class) +public final class EmailAddress { + + private static final String EMAIL_REGEX = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; + private static final Pattern PATTERN = Pattern.compile(EMAIL_REGEX); + private final String value; + + /** + * Creates a new {@link EmailAddress} from the given {@link String} representation. + * + * @param emailAddress + * must not be {@literal null} or empty. + */ + @JsonCreator + public EmailAddress(String emailAddress) { + Assert.isTrue(isValid(emailAddress), "Invalid email address!"); + this.value = emailAddress; + } + + /** + * Returns whether the given {@link String} is a valid {@link EmailAddress} which means you can safely instantiate + * the + * class. + * + * @param candidate + * + * @return + */ + public static boolean isValid(String candidate) { + return candidate == null ? false : PATTERN.matcher(candidate).matches(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return value; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if(this == obj) { + return true; + } + + if(!(obj instanceof EmailAddress)) { + return false; + } + + EmailAddress that = (EmailAddress)obj; + return this.value.equals(that.value); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + @Component + static class EmailAddressToStringConverter implements Converter { + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public String convert(EmailAddress source) { + return source == null ? null : source.value; + } + } + + @Component + static class StringToEmailAddressConverter implements Converter { + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + public EmailAddress convert(String source) { + return StringUtils.hasText(source) ? new EmailAddress(source) : null; + } + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Product.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Product.java new file mode 100644 index 000000000..98b543856 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/Product.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.gemfire.mapping.Region; +import org.springframework.util.Assert; + + +/** + * A product. + * + * @author Oliver Gierke + * @author David Turanski + */ +@Region +public class Product extends AbstractPersistentEntity { + + private String name, description; + private BigDecimal price; + private Map attributes = new HashMap(); + + /** + * Creates a new {@link Product} with the given name. + * + * @param id + * a unique Id + * @param name + * must not be {@literal null} or empty. + * @param price + * must not be {@literal null} or less than or equal to zero. + */ + public Product(Long id, String name, BigDecimal price) { + this(id, name, price, null); + } + + /** + * Creates a new {@link Product} from the given name and description. + * + * @param id + * a unique Id + * @param name + * must not be {@literal null} or empty. + * @param price + * must not be {@literal null} or less than or equal to zero. + * @param description + */ + @PersistenceConstructor + public Product(Long id, String name, BigDecimal price, String description) { + super(id); + Assert.hasText(name, "Name must not be null or empty!"); + Assert.isTrue(BigDecimal.ZERO.compareTo(price) < 0, "Price must be greater than zero!"); + + this.name = name; + this.price = price; + this.description = description; + } + + protected Product() { + } + + /** + * Sets the attribute with the given name to the given value. + * + * @param name + * must not be {@literal null} or empty. + * @param value + */ + public void setAttribute(String name, String value) { + + Assert.hasText(name); + + if(value == null) { + this.attributes.remove(value); + } else { + this.attributes.put(name, value); + } + } + + /** + * Returns the {@link Product}'s name. + * + * @return + */ + public String getName() { + return name; + } + + /** + * Returns the {@link Product}'s description. + * + * @return + */ + public String getDescription() { + return description; + } + + /** + * Returns all the custom attributes of the {@link Product}. + * + * @return + */ + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + /** + * Returns the price of the {@link Product}. + * + * @return + */ + public BigDecimal getPrice() { + return price; + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/ProductRepository.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/ProductRepository.java new file mode 100644 index 000000000..484acd3c9 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/core/ProductRepository.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.core; + +import java.util.List; + +import org.springframework.data.gemfire.repository.Query; +import org.springframework.data.repository.CrudRepository; + + +/** + * Repository interface to access {@link Product}s. + * + * @author Oliver Gierke + * @author David TuranskiGem + */ +public interface ProductRepository extends CrudRepository { + + /** + * Returns a list of {@link Product}s having a description which contains the given snippet. + * @param the search string + * @return + */ + + List findByDescriptionContaining(String description); + + /** + * Returns all {@link Product}s having the given attribute value. + * @param attribute + * @param value + * @return + */ + @Query("SELECT * FROM /Product where attributes[$1] = $2") + List findByAttributes(String key, String value); + + List findByName(String name); +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/LineItem.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/LineItem.java new file mode 100644 index 000000000..2259f415a --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/LineItem.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.order; + +import java.math.BigDecimal; + +import org.springframework.util.Assert; + +import org.springframework.data.rest.example.gemfire.core.Product; + +/** + * @author Oliver Gierke + */ +public class LineItem { + + private BigDecimal price; + private int amount; + private Long productId; + + /** + * Creates a new {@link LineItem} for the given {@link Product}. + * @param product must not be {@literal null}. + */ + public LineItem(Product product) { + this(product, 1); + } + + /** + * Creates a new {@link LineItem} for the given {@link Product} and amount. + * @param product must not be {@literal null}. + * @param amount + */ + public LineItem(Product product, int amount) { + Assert.notNull(product, "The given Product must not be null!"); + Assert.isTrue(amount > 0, "The amount of Products to be bought must be greater than 0!"); + + this.productId = product.getId(); + this.amount = amount; + this.price = product.getPrice(); + } + + protected LineItem() { + } + + /** + * Returns the id of the {@link Product} the {@link LineItem} refers to. + * + * @return + */ + public Long getProductId() { + return productId; + } + + /** + * Returns the amount of {@link Product}s to be ordered. + * + * @return + */ + public int getAmount() { + return amount; + } + + /** + * Returns the price a single unit of the {@link LineItem}'s product. + * + * @return the price + */ + public BigDecimal getUnitPrice() { + return price; + } + + /** + * Returns the total for the {@link LineItem}. + * + * @return + */ + public BigDecimal getTotal() { + return price.multiply(BigDecimal.valueOf(amount)); + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/Order.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/Order.java new file mode 100644 index 000000000..34edba895 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/Order.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.order; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.gemfire.mapping.Region; +import org.springframework.data.rest.example.gemfire.core.AbstractPersistentEntity; +import org.springframework.data.rest.example.gemfire.core.Address; +import org.springframework.data.rest.example.gemfire.core.Customer; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + * @author David Turanski + */ +@Region +public class Order extends AbstractPersistentEntity { + + private Long customerId; + private Address billingAddress; + private Address shippingAddress; + private Set lineItems = new HashSet(); + + /** + * Creates a new {@link Order} for the given {@link Customer}. + * + * @param customer + * must not be {@literal null}. + */ + public Order(Long id, Long customerId, Address shippingAddress) { + super(id); + Assert.notNull(customerId); + Assert.notNull(shippingAddress); + + this.customerId = customerId; + this.shippingAddress = shippingAddress; + } + + protected Order() { + } + + /** + * Adds the given {@link LineItem} to the {@link Order}. + * + * @param lineItem + */ + public void add(LineItem lineItem) { + this.lineItems.add(lineItem); + } + + /** + * Returns the id of the {@link Customer} who placed the {@link Order}. + * + * @return + */ + public Long getCustomerId() { + return customerId; + } + + /** + * Returns the billing {@link Address} for this order. + * + * @return + */ + public Address getBillingAddress() { + return billingAddress != null ? billingAddress : shippingAddress; + } + + /** + * Returns the shipping {@link Address} for this order; + * + * @return + */ + public Address getShippingAddress() { + return shippingAddress; + } + + /** + * Returns all {@link LineItem}s currently belonging to the {@link Order}. + * + * @return + */ + public Set getLineItems() { + return Collections.unmodifiableSet(lineItems); + } + + /** + * Returns the total of the {@link Order}. + * + * @return + */ + public BigDecimal getTotal() { + + BigDecimal total = BigDecimal.ZERO; + + for(LineItem item : lineItems) { + total = total.add(item.getTotal()); + } + + return total; + } +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/OrderRepository.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/OrderRepository.java new file mode 100644 index 000000000..8d550334d --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/gemfire/order/OrderRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.example.gemfire.order; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Oliver Gierke + * @author David Turanski + */ +public interface OrderRepository extends CrudRepository { + List findByCustomerId(Long customerId); +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/JpaRepositoryConfig.java similarity index 83% rename from spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java rename to spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/JpaRepositoryConfig.java index ceac6c0fd..9b5fdb4ae 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/JpaRepositoryConfig.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.repository.test; +package org.springframework.data.rest.example.jpa; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; @@ -7,7 +7,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.jpa.JpaDialect; @@ -23,10 +22,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * @author Jon Brisbin */ @Configuration -@ComponentScan(basePackageClasses = {ApplicationConfig.class}) +@ComponentScan(basePackageClasses = JpaRepositoryConfig.class) @EnableJpaRepositories @EnableTransactionManagement -public class ApplicationConfig { +public class JpaRepositoryConfig { @Bean public DataSource dataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); @@ -42,7 +41,6 @@ public class ApplicationConfig { factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan(getClass().getPackage().getName()); factory.setDataSource(dataSource()); - factory.setPersistenceXmlLocation("/JpaMetadataSpec-persistence.xml"); factory.afterPropertiesSet(); @@ -59,8 +57,4 @@ public class ApplicationConfig { return txManager; } - @Bean public JpaRepositoryExporter jpaRepositoryExporter() { - return new JpaRepositoryExporter(); - } - } diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/Person.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/Person.java new file mode 100644 index 000000000..cb261a4c4 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/Person.java @@ -0,0 +1,106 @@ +package org.springframework.data.rest.example.jpa; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.PrePersist; +import javax.validation.constraints.NotNull; + +import org.springframework.data.rest.repository.annotation.Description; + +/** + * An entity that represents a person. + * + * @author Jon Brisbin + */ +@Entity +public class Person { + + private Long id; + @Description("A person's first name") + private String firstName; + @Description("A person's last name") + private String lastName; + @Description("A person's siblings") + private List siblings = Collections.emptyList(); + private Person father; + @Description("Timestamp this person object was created") + private Date created; + + public Person() { + } + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Id @GeneratedValue public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + @NotNull + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Person addSibling(Person p) { + if(siblings == Collections.EMPTY_LIST) { + siblings = new ArrayList(); + } + siblings.add(p); + return this; + } + + @ManyToMany public List getSiblings() { + return siblings; + } + + public void setSiblings(List siblings) { + this.siblings = siblings; + } + + @ManyToOne public Person getFather() { + return father; + } + + public void setFather(Person father) { + this.father = father; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + } + + @PrePersist + private void prePersist() { + this.created = Calendar.getInstance().getTime(); + } + +} \ No newline at end of file diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonLoader.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonLoader.java new file mode 100644 index 000000000..5dabb5b12 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonLoader.java @@ -0,0 +1,31 @@ +package org.springframework.data.rest.example.jpa; + +import java.util.Arrays; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Jon Brisbin + */ +@Component +public class PersonLoader implements InitializingBean { + + @Autowired + PersonRepository people; + + @Override public void afterPropertiesSet() throws Exception { + Person billyBob = people.save(new Person("Billy Bob", "Thornton")); + + Person john = new Person("John", "Doe"); + Person jane = new Person("Jane", "Doe"); + john.addSibling(jane); + john.setFather(billyBob); + jane.addSibling(john); + jane.setFather(billyBob); + + people.save(Arrays.asList(john, jane)); + } + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonRepository.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonRepository.java new file mode 100644 index 000000000..c30b084a0 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/jpa/PersonRepository.java @@ -0,0 +1,36 @@ +package org.springframework.data.rest.example.jpa; + +import java.util.Date; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.convert.ISO8601DateConverter; +import org.springframework.data.rest.repository.annotation.ConvertWith; +import org.springframework.data.rest.repository.annotation.RestResource; + +/** + * A repository to manage {@link Person}s. + * + * @author Jon Brisbin + */ +@RestResource(rel = "people", path = "people") +public interface PersonRepository extends PagingAndSortingRepository { + + @RestResource(rel = "firstname", path = "firstname") + public Page findByFirstName(@Param("firstName") String firstName, Pageable pageable); + + public Person findFirstPersonByFirstName(@Param("firstName") String firstName); + + public Page findByCreatedGreaterThan(@Param("date") Date date, Pageable pageable); + + @Query("select p from Person p where p.created > :date") + public Page findByCreatedUsingISO8601Date(@Param("date") + @ConvertWith( + ISO8601DateConverter.class) + Date date, + Pageable pageable); + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/MongoDbRepositoryConfig.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/MongoDbRepositoryConfig.java new file mode 100644 index 000000000..86a1516ce --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/MongoDbRepositoryConfig.java @@ -0,0 +1,30 @@ +package org.springframework.data.rest.example.mongodb; + +import java.net.UnknownHostException; + +import com.mongodb.Mongo; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +/** + * @author Jon Brisbin + */ +@Configuration +@ComponentScan(basePackageClasses = MongoDbRepositoryConfig.class) +@EnableMongoRepositories +public class MongoDbRepositoryConfig { + + @Bean public MongoDbFactory mongoDbFactory() throws UnknownHostException { + return new SimpleMongoDbFactory(new Mongo("localhost"), "spring-data-rest-example"); + } + + @Bean public MongoTemplate mongoTemplate() throws UnknownHostException { + return new MongoTemplate(mongoDbFactory()); + } + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/Profile.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/Profile.java new file mode 100644 index 000000000..724b2d388 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/Profile.java @@ -0,0 +1,44 @@ +package org.springframework.data.rest.example.mongodb; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * @author Jon Brisbin + */ +@Document +public class Profile { + + @Id + private String id; + private Long person; + private String type; + + public String getId() { + return id; + } + + public Profile setId(String id) { + this.id = id; + return this; + } + + public Long getPerson() { + return person; + } + + public Profile setPerson(Long person) { + this.person = person; + return this; + } + + public String getType() { + return type; + } + + public Profile setType(String type) { + this.type = type; + return this; + } + +} diff --git a/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/ProfileRepository.java b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/ProfileRepository.java new file mode 100644 index 000000000..a0fc07a61 --- /dev/null +++ b/spring-data-rest-example/src/main/java/org/springframework/data/rest/example/mongodb/ProfileRepository.java @@ -0,0 +1,9 @@ +package org.springframework.data.rest.example.mongodb; + +import org.springframework.data.repository.CrudRepository; + +/** + * @author Jon Brisbin + */ +public interface ProfileRepository extends CrudRepository { +} diff --git a/spring-data-rest-repository/src/test/resources/JpaMetadataSpec-persistence.xml b/spring-data-rest-example/src/main/resources/META-INF/persistence.xml similarity index 82% rename from spring-data-rest-repository/src/test/resources/JpaMetadataSpec-persistence.xml rename to spring-data-rest-example/src/main/resources/META-INF/persistence.xml index 56846a7f4..a92748d1c 100644 --- a/spring-data-rest-repository/src/test/resources/JpaMetadataSpec-persistence.xml +++ b/spring-data-rest-example/src/main/resources/META-INF/persistence.xml @@ -1,8 +1,6 @@ - org.springframework.data.rest.repository.test.Person - org.springframework.data.rest.repository.test.Family diff --git a/spring-data-rest-example/src/main/resources/META-INF/spring/cache-config.xml b/spring-data-rest-example/src/main/resources/META-INF/spring/cache-config.xml new file mode 100644 index 000000000..a991aebfc --- /dev/null +++ b/spring-data-rest-example/src/main/resources/META-INF/spring/cache-config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-data-rest-example/src/main/resources/META-INF/spring/security-config.xml b/spring-data-rest-example/src/main/resources/META-INF/spring/security-config.xml new file mode 100644 index 000000000..96a6fa0f3 --- /dev/null +++ b/spring-data-rest-example/src/main/resources/META-INF/spring/security-config.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/logback.xml b/spring-data-rest-example/src/main/resources/logback.xml similarity index 74% rename from spring-data-rest-webmvc/src/test/resources/logback.xml rename to spring-data-rest-example/src/main/resources/logback.xml index 9b14dc1f4..905c91963 100644 --- a/spring-data-rest-webmvc/src/test/resources/logback.xml +++ b/spring-data-rest-example/src/main/resources/logback.xml @@ -8,9 +8,7 @@ - - diff --git a/spring-data-rest-repository/build.gradle b/spring-data-rest-repository/build.gradle deleted file mode 100644 index 66fefc81a..000000000 --- a/spring-data-rest-repository/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -dependencies { - - // Spring - //compile("org.springframework:spring-orm:$springVersion") { force = true } - //compile("org.springframework:spring-oxm:$springVersion") { force = true } - - // 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" - - // Spring HATEOAS - compile "org.springframework.hateoas:spring-hateoas:$hateoasVersion" - - // Exporter core - compile project(":spring-data-rest-core") - - // Testing - testCompile "org.hibernate:hibernate-entitymanager:$hibernateVersion" - testCompile "org.hsqldb:hsqldb:2.2.8" - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java similarity index 57% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java rename to spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java index 321d58299..d0e45fbf1 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java @@ -1,6 +1,7 @@ -package org.springframework.data.rest.webmvc; +package org.springframework.data.rest.config; import java.net.URI; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -10,15 +11,10 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; /** - * Central configuration helper class for the REST exporter. If something within the REST exporter is configurable, - * there is a property here you can use to set the value. - * * @author Jon Brisbin */ public class RepositoryRestConfiguration { - public static final RepositoryRestConfiguration DEFAULT = new RepositoryRestConfiguration(); - private URI baseUri = null; private int defaultPageSize = 20; private String pageParamName = "page"; @@ -30,11 +26,14 @@ public class RepositoryRestConfiguration { private Map, Class> typeMappings = Collections.emptyMap(); private MediaType defaultMediaType = MediaType.APPLICATION_JSON; private boolean dumpErrors = true; + private List> exposeIdsFor = new ArrayList>(); + private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration(); + private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration(); /** * The base URI against which the exporter should calculate its links. * - * @return + * @return The base URI. */ public URI getBaseUri() { return baseUri; @@ -44,6 +43,7 @@ public class RepositoryRestConfiguration { * The base URI against which the exporter should calculate its links. * * @param baseUri + * The base URI. */ public RepositoryRestConfiguration setBaseUri(URI baseUri) { Assert.notNull(baseUri, "The baseUri cannot be null."); @@ -54,7 +54,7 @@ public class RepositoryRestConfiguration { /** * Get the default size of {@link org.springframework.data.domain.Pageable}s. Default is 20. * - * @return + * @return The default page size. */ public int getDefaultPageSize() { return defaultPageSize; @@ -64,8 +64,9 @@ public class RepositoryRestConfiguration { * Set the default size of {@link org.springframework.data.domain.Pageable}s. * * @param defaultPageSize + * The default page size. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setDefaultPageSize(int defaultPageSize) { Assert.isTrue((defaultPageSize > 0), "Page size must be greater than 0."); @@ -76,7 +77,7 @@ public class RepositoryRestConfiguration { /** * Get the name of the URL query string parameter that indicates what page to return. Default is 'page'. * - * @return + * @return Name of the query parameter used to indicate the page number to return. */ public String getPageParamName() { return pageParamName; @@ -86,8 +87,9 @@ public class RepositoryRestConfiguration { * Set the name of the URL query string parameter that indicates what page to return. * * @param pageParamName + * Name of the query parameter used to indicate the page number to return. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setPageParamName(String pageParamName) { Assert.notNull(pageParamName, "Page param name cannot be null."); @@ -99,7 +101,7 @@ public class RepositoryRestConfiguration { * Get the name of the URL query string parameter that indicates how many results to return at once. Default is * 'limit'. * - * @return + * @return Name of the query parameter used to indicate the maximum number of entries to return at a time. */ public String getLimitParamName() { return limitParamName; @@ -109,8 +111,9 @@ public class RepositoryRestConfiguration { * Set the name of the URL query string parameter that indicates how many results to return at once. * * @param limitParamName + * Name of the query parameter used to indicate the maximum number of entries to return at a time. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setLimitParamName(String limitParamName) { Assert.notNull(limitParamName, "Limit param name cannot be null."); @@ -121,7 +124,7 @@ public class RepositoryRestConfiguration { /** * Get the name of the URL query string parameter that indicates what direction to sort results. Default is 'sort'. * - * @return + * @return Name of the query string parameter used to indicate what field to sort on. */ public String getSortParamName() { return sortParamName; @@ -131,8 +134,9 @@ public class RepositoryRestConfiguration { * Set the name of the URL query string parameter that indicates what direction to sort results. * * @param sortParamName + * Name of the query string parameter used to indicate what field to sort on. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setSortParamName(String sortParamName) { Assert.notNull(sortParamName, "Sort param name cannot be null."); @@ -143,7 +147,7 @@ public class RepositoryRestConfiguration { /** * Get the list of custom {@link HttpMessageConverter}s to be used to convert user input to objects and visa versa. * - * @return + * @return List of custom {@literal HttpMessageConverter}s. */ public List> getCustomConverters() { return customConverters; @@ -153,8 +157,9 @@ public class RepositoryRestConfiguration { * Set the list of custom {@link HttpMessageConverter}s to be used to convert user input to objects and visa versa. * * @param customConverters + * List of custom {@literal HttpMessageConverter}s. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setCustomConverters(List> customConverters) { Assert.notNull(customConverters, "Custom converters list cannot be null."); @@ -166,7 +171,7 @@ public class RepositoryRestConfiguration { * Get the list of domain type to repository implementation mappings that will help the exporters narrow down the * correct {@link org.springframework.data.repository.Repository} to return for a given domain type. * - * @return + * @return A {@link Map} of domain type to repository mappings. */ public Map, Class> getDomainTypeToRepositoryMappings() { return typeMappings; @@ -177,8 +182,9 @@ public class RepositoryRestConfiguration { * correct {@link org.springframework.data.repository.Repository} to return for a given domain type. * * @param typeMappings + * A {@link Map} of domain type to repository mappings. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setDomainTypeToRepositoryMappings(Map, Class> typeMappings) { this.typeMappings = typeMappings; @@ -189,7 +195,7 @@ public class RepositoryRestConfiguration { * Get the name of the URL query string parameter that indicates the name of the javascript function to use as the * JSONP wrapper for results. * - * @return + * @return Name of the query string parameter used to indicate the JSONP callback function. */ public String getJsonpParamName() { return jsonpParamName; @@ -200,8 +206,9 @@ public class RepositoryRestConfiguration { * JSONP wrapper for results. * * @param jsonpParamName + * Name of the query string parameter used to indicate the JSONP callback function. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setJsonpParamName(String jsonpParamName) { this.jsonpParamName = jsonpParamName; @@ -212,7 +219,8 @@ public class RepositoryRestConfiguration { * Get the name of the URL query string parameter that indicates the name of the javascript function to use as the * error handler JSONP wrapper for errors. * - * @return + * @return Name of the query string parameter used to indicate what javascript function to use as the JSONP error + * response. */ public String getJsonpOnErrParamName() { return jsonpOnErrParamName; @@ -223,8 +231,10 @@ public class RepositoryRestConfiguration { * error handler JSONP wrapper for errors. * * @param jsonpOnErrParamName + * Name of the query string parameter used to indicate what javascript function to use as the JSONP error + * response. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setJsonpOnErrParamName(String jsonpOnErrParamName) { this.jsonpOnErrParamName = jsonpOnErrParamName; @@ -234,7 +244,7 @@ public class RepositoryRestConfiguration { /** * Get the {@link MediaType} to use as a default when none is specified. * - * @return + * @return Default content type if none has been specified. */ public MediaType getDefaultMediaType() { return defaultMediaType; @@ -244,8 +254,9 @@ public class RepositoryRestConfiguration { * Set the {@link MediaType} to use as a default when none is specified. * * @param defaultMediaType + * Default content type if none has been specified. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setDefaultMediaType(MediaType defaultMediaType) { this.defaultMediaType = defaultMediaType; @@ -255,7 +266,7 @@ public class RepositoryRestConfiguration { /** * Should exception messages be logged to the body of the response in a JSON object? * - * @return + * @return Flag indicating whether exception messages are logged to the body of the response. */ public boolean isDumpErrors() { return dumpErrors; @@ -265,12 +276,106 @@ public class RepositoryRestConfiguration { * Set whether exception messages should be logged to the body of the response as a JSON object. * * @param dumpErrors + * Flag indicating whether exception messages are logged to the body of the response. * - * @return + * @return {@literal this} */ public RepositoryRestConfiguration setDumpErrors(boolean dumpErrors) { this.dumpErrors = dumpErrors; return this; } + /** + * Start configuration a {@link ResourceMapping} for a specific domain type. + * + * @param domainType + * The {@link Class} of the domain type to configure a mapping for. + * + * @return A new {@link ResourceMapping} for configuring how a domain type is mapped. + */ + public ResourceMapping addResourceMappingForDomainType(Class domainType) { + return domainMappings.addResourceMappingFor(domainType); + } + + /** + * Get the {@link ResourceMapping} for a specific domain type. + * + * @param domainType + * The {@link Class} of the domain type. + * + * @return A {@link ResourceMapping} for that domain type or {@literal null} if none exists. + */ + public ResourceMapping getResourceMappingForDomainType(Class domainType) { + return domainMappings.getResourceMappingFor(domainType); + } + + public boolean hasResourceMappingForDomainType(Class domainType) { + return domainMappings.hasResourceMappingFor(domainType); + } + + public ResourceMappingConfiguration getDomainTypesResourceMappingConfiguration() { + return domainMappings; + } + + /** + * Start configuration a {@link ResourceMapping} for a specific repository interface. + * + * @param repositoryInterface + * The {@link Class} of the repository interface to configure a mapping for. + * + * @return A new {@link ResourceMapping} for configuring how a repository interface is mapped. + */ + public ResourceMapping setResourceMappingForRepository(Class repositoryInterface) { + return repoMappings.addResourceMappingFor(repositoryInterface); + } + + /** + * Get the {@link ResourceMapping} for a specific repository interface. + * + * @param repositoryInterface + * The {@link Class} of the repository interface. + * + * @return A {@link ResourceMapping} for that repository interface or {@literal null} if none exists. + */ + public ResourceMapping getResourceMappingForRepository(Class repositoryInterface) { + return repoMappings.getResourceMappingFor(repositoryInterface); + } + + public boolean hasResourceMappingForRepository(Class repositoryInterface) { + return repoMappings.hasResourceMappingFor(repositoryInterface); + } + + public ResourceMapping findRepositoryMappingForPath(String path) { + Class type = repoMappings.findTypeForPath(path); + if(null == type) { + return null; + } + return repoMappings.getResourceMappingFor(type); + } + + /** + * Should we expose the ID property for this domain type? + * + * @param domainType + * The domain type we may need to expose the ID for. + * + * @return {@literal true} is the ID is to be exposed, {@literal false} otherwise. + */ + public boolean isIdExposedFor(Class domainType) { + return exposeIdsFor.contains(domainType); + } + + /** + * Set the list of domain types for which we will expose the ID value as a normal property. + * + * @param domainTypes + * Array of types to expose IDs for. + * + * @return {@literal this} + */ + public RepositoryRestConfiguration exposeIdsFor(Class... domainTypes) { + Collections.addAll(exposeIdsFor, domainTypes); + return this; + } + } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java new file mode 100644 index 000000000..d43a3b0e7 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java @@ -0,0 +1,110 @@ +package org.springframework.data.rest.config; + +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Jon Brisbin + */ +public class ResourceMapping { + + private String rel; + private String path; + private boolean exported = true; + private final Map resourceMappings = new HashMap(); + + public ResourceMapping() { + } + + public ResourceMapping(Class type) { + rel = findRel(type); + path = findPath(type); + exported = findExported(type); + } + + public ResourceMapping(String rel, String path) { + this.rel = rel; + this.path = path; + } + + public ResourceMapping(String rel, String path, boolean exported) { + this.rel = rel; + this.path = path; + this.exported = exported; + } + + public String getRel() { + return rel; + } + + public ResourceMapping setRel(String rel) { + this.rel = rel; + return this; + } + + public String getPath() { + return path; + } + + public ResourceMapping setPath(String path) { + this.path = path; + return this; + } + + public boolean isExported() { + return exported; + } + + public ResourceMapping setExported(boolean exported) { + this.exported = exported; + return this; + } + + public ResourceMapping addResourceMappings(Map mappings) { + if(null == mappings) { + return this; + } + + resourceMappings.putAll(mappings); + return this; + } + + public ResourceMapping addResourceMappingFor(String name) { + ResourceMapping rm = new ResourceMapping(); + resourceMappings.put(name, rm); + return rm; + } + + public ResourceMapping getResourceMappingFor(String name) { + return resourceMappings.get(name); + } + + public boolean hasResourceMappingFor(String name) { + return resourceMappings.containsKey(name); + } + + public Map getResourceMappings() { + return resourceMappings; + } + + public String getNameForPath(String path) { + for(Map.Entry mapping : resourceMappings.entrySet()) { + if(mapping.getValue().getPath().equals(path)) { + return mapping.getKey(); + } + } + return path; + } + + @Override public String toString() { + return "ResourceMapping{" + + "rel='" + rel + '\'' + + ", path='" + path + '\'' + + ", exported=" + exported + + ", resourceMappings=" + resourceMappings + + '}'; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java new file mode 100644 index 000000000..118326eff --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java @@ -0,0 +1,45 @@ +package org.springframework.data.rest.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages the {@link ResourceMapping} configurations for any resources being exported. This includes domain entities + * and repositories. + * + * @author Jon Brisbin + */ +public class ResourceMappingConfiguration { + + private final Map, ResourceMapping> resourceMappings = new HashMap, ResourceMapping>(); + + public ResourceMapping addResourceMappingFor(Class type) { + ResourceMapping rm = resourceMappings.get(type); + if(null == rm) { + rm = new ResourceMapping(type); + resourceMappings.put(type, rm); + } + return rm; + } + + public ResourceMapping getResourceMappingFor(Class type) { + return resourceMappings.get(type); + } + + public boolean hasResourceMappingFor(Class type) { + return resourceMappings.containsKey(type); + } + + public Class findTypeForPath(String path) { + if(null == path) { + return null; + } + for(Map.Entry, ResourceMapping> entry : resourceMappings.entrySet()) { + if(path.equals(entry.getValue().getPath())) { + return entry.getKey(); + } + } + return null; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java deleted file mode 100644 index 404b2d435..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Map; -import java.util.Set; - -/** - * Encapsulates necessary information about an attribute of a generic entity. - * - * @author Jon Brisbin - */ -public interface AttributeMetadata { - - /** - * Name of the attribute. - * - * @return name of the attribute. - */ - String name(); - - /** - * The type of this attribute. - * - * @return type of this attribute. - */ - Class type(); - - /** - * The type of this map's key, if it's map-like. - * - * @return - */ - Class keyType(); - - /** - * The element type of this attribute, if this attribute is a "plural"-like attribute (a Collection, Map, etc...). - * - * @return Class of element type or {@literal null} if not a plural attribute. - */ - Class elementType(); - - /** - * Whether this attribute can be nulled or not. - * - * @return - */ - boolean isNullable(); - - /** - * Can this attribute look like a {@link Collection}? - * - * @return {@literal true} if attribute is a Collection, {@literal false} otherwise. - */ - boolean isCollectionLike(); - - /** - * Get the path of this attribute as a {@link Collection}. - * - * @param target - * The entity to inspect for this attribute. - * - * @return attribute value as a {@link Collection} - */ - Collection asCollection(Object target); - - /** - * Can this attribute look like a {@link Set}? - * - * @return {@literal true} if attribute is a Set, {@literal false} otherwise. - */ - boolean isSetLike(); - - /** - * Get the path of this attribute as a {@link Set}. - * - * @param target - * The entity to inspect for this attribute. - * - * @return attribute value as a {@link Set} - */ - Set asSet(Object target); - - /** - * Can this attribute look like a {@link Map}? - * - * @return {@literal true} if attribute is a Map, {@literal false} otherwise. - */ - boolean isMapLike(); - - /** - * Get the path of this attribute as a {@link Map}. - * - * @param target - * The entity to inspect for this attribute. - * - * @return attribute value as a {@link Map} - */ - Map asMap(Object target); - - /** - * Does this attribute have the given annotation on it? - * - * @param annoType - * The type of annotation to search for. - * - * @return {@literal true} if this annotation exists on this attribute, {@literal false} otherwise. - */ - boolean hasAnnotation(Class annoType); - - /** - * Get the given annotation. - * - * @param annoType - * The type of annotation to get. - * @param - * - * @return The annotation, or {@literal null} if it doesn't exist. - */ - A annotation(Class annoType); - - /** - * Get the path of this attribute. - * - * @param target - * The entity to inspect for this attribute. - * - * @return attribute value - */ - Object get(Object target); - - /** - * Set the path of this attribute. - * - * @param value - * Value to set on this attribute. - * @param target - * The entity to set this attribute's value on. - * - * @return @this - */ - AttributeMetadata set(Object value, Object target); - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java new file mode 100644 index 000000000..be39a50f9 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java @@ -0,0 +1,66 @@ +package org.springframework.data.rest.repository; + +import static org.springframework.data.rest.core.util.UriUtils.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; + +/** + * @author Jon Brisbin + */ +public class BaseUriAwareResource extends Resource { + + @JsonIgnore + private URI baseUri; + + public BaseUriAwareResource() { + } + + public BaseUriAwareResource(T content, Link... links) { + super(content, links); + } + + public BaseUriAwareResource(T content, Iterable links) { + super(content, links); + } + + public URI getBaseUri() { + return baseUri; + } + + public BaseUriAwareResource setBaseUri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + @Override public List getLinks() { + String baseUriStr = baseUri.toString(); + List links = new ArrayList(); + for(Link l : super.getLinks()) { + if(!l.getHref().startsWith(baseUriStr)) { + links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel())); + } else { + links.add(l); + } + } + return links; + } + + @Override public Link getLink(String rel) { + Link l = super.getLink(rel); + if(null == l) { + return null; + } + if(!l.getHref().startsWith(baseUri.toString())) { + return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()); + } else { + return l; + } + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java new file mode 100644 index 000000000..705a92eba --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java @@ -0,0 +1,73 @@ +package org.springframework.data.rest.repository; + +import static org.springframework.data.rest.core.util.UriUtils.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; + +/** + * @author Jon Brisbin + */ +public class BaseUriAwareResources extends Resources> { + + @JsonIgnore + private URI baseUri; + + public BaseUriAwareResources() { + } + + public BaseUriAwareResources(Iterable> content, Link... links) { + super(content, links); + } + + public BaseUriAwareResources(Iterable> content, Iterable links) { + super(content, links); + } + + public URI getBaseUri() { + return baseUri; + } + + public BaseUriAwareResources setBaseUri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + @Override public Collection> getContent() { + List> resources = new ArrayList>(); + for(Resource resource : super.getContent()) { + if(resource instanceof BaseUriAwareResource) { + resources.add(((BaseUriAwareResource)resource).setBaseUri(baseUri)); + } else { + resources.add(new BaseUriAwareResource(resource.getContent(), resource.getLinks()).setBaseUri(baseUri)); + } + } + return resources; + } + + @Override public Iterator> iterator() { + return getContent().iterator(); + } + + @Override public List getLinks() { + List links = new ArrayList(); + for(Link l : super.getLinks()) { + links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel())); + } + return links; + } + + @Override public Link getLink(String rel) { + Link l = super.getLink(rel); + return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()); + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java deleted file mode 100644 index 9bd40d63b..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.util.Map; - -/** - * Encapsulates necessary metadata about a generic entity. - * - * @author Jon Brisbin - */ -public interface EntityMetadata { - - /** - * The class of this entity. - * - * @return Type of this domain class. - */ - Class type(); - - /** - * A Map of attribute metadata keyed on the attribute's name. - * - * @return Attributes that do not involve relationships. - */ - Map embeddedAttributes(); - - /** - * A Map of linked attribute metadata keyed on the attribute's name. - * - * @return Attributes that involve relationships. - */ - Map linkedAttributes(); - - /** - * The {@link AttributeMetadata} representing the ID of the entity. - * - * @return {@link AttributeMetadata} for the ID. - */ - A idAttribute(); - - /** - * The {@link AttributeMetadata} representing the version of the entity, if applicable. - * - * @return {@link AttributeMetadata} or {@literal null} if no version attributes exists. - */ - A versionAttribute(); - - /** - * Get {@link AttributeMetadata} by name. - * - * @param name - * The name of the attribute. - * - * @return {@link AttributeMetadata} or {@literal null} if that attribute doesn't exist. - */ - A attribute(String name); - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java deleted file mode 100644 index 795c6dc45..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.springframework.data.rest.repository; - -/** - * @author Jon Brisbin - */ -public class PagingMetadata { - - private int number = 0; - private int size = 0; - private int totalPages = 0; - private long totalElements = 0; - - public PagingMetadata() { - } - - public PagingMetadata(int number, - int size, - int totalPages, - long totalElements) { - this.number = number; - this.size = size; - this.totalPages = totalPages; - this.totalElements = totalElements; - } - - public int getNumber() { - return number; - } - - public PagingMetadata setNumber(int number) { - this.number = number; - return this; - } - - public int getSize() { - return size; - } - - public PagingMetadata setSize(int size) { - this.size = size; - return this; - } - - public int getTotalPages() { - return totalPages; - } - - public PagingMetadata setTotalPages(int totalPages) { - this.totalPages = totalPages; - return this; - } - - public long getTotalElements() { - return totalElements; - } - - public PagingMetadata setTotalElements(long totalElements) { - this.totalElements = totalElements; - return this; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java new file mode 100644 index 000000000..2d0650788 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java @@ -0,0 +1,51 @@ +package org.springframework.data.rest.repository; + +import java.net.URI; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; + +/** + * A Spring HATEOAS {@link Resource} subclass that holds a reference to the entity's {@link PersistentEntity} metadata. + * + * @author Jon Brisbin + */ +public class PersistentEntityResource extends BaseUriAwareResource { + + @JsonIgnore + private final PersistentEntity persistentEntity; + + @SuppressWarnings({"unchecked"}) + public static PersistentEntityResource wrap(PersistentEntity persistentEntity, + T obj, + URI baseUri) { + PersistentEntityResource resource = new PersistentEntityResource(persistentEntity, obj); + resource.setBaseUri(baseUri); + return resource; + } + + public PersistentEntityResource(PersistentEntity persistentEntity) { + this.persistentEntity = persistentEntity; + } + + public PersistentEntityResource(PersistentEntity persistentEntity, + T content, + Link... links) { + super(content, links); + this.persistentEntity = persistentEntity; + } + + public PersistentEntityResource(PersistentEntity persistentEntity, + T content, + Iterable links) { + super(content, links); + this.persistentEntity = persistentEntity; + } + + public PersistentEntity getPersistentEntity() { + return persistentEntity; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java index bcdcf811a..66f26f028 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java @@ -4,10 +4,11 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.validation.Errors; /** - * @author Jon Brisbin + * Exception that is thrown when a Spring {@link org.springframework.validation.Validator} throws an error. + * + * @author Jon Brisbin */ -public class RepositoryConstraintViolationException - extends DataIntegrityViolationException { +public class RepositoryConstraintViolationException extends DataIntegrityViolationException { private Errors errors; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java deleted file mode 100644 index a58e4ab48..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -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.support.Repositories; -import org.springframework.data.rest.repository.annotation.RestResource; -import org.springframework.util.StringUtils; - -/** - * Abstract class that contains the basic functionality that any exporter will need - * to export a Repository implementation. - * - * @author Jon Brisbin - */ -public abstract class RepositoryExporter, M extends RepositoryMetadata, E extends EntityMetadata> - implements ApplicationContextAware, - InitializingBean { - - protected ApplicationContext applicationContext; - protected Repositories repositories; - protected Map repositoryMetadata; - protected List exportOnlyTheseClasses = Collections.emptyList(); - protected Map, Class> domainTypeMappings = new HashMap, Class>(); - - /** - * Get the list of class names of Repositories to export. - * - * @return a List of class names to export - */ - public List getExportOnlyTheseClasses() { - return exportOnlyTheseClasses; - } - - /** - * Set the class names of only those Repositories you want exported. - * Default is to export all found Repositories. - * - * @param exportOnlyTheseClasses - * {@link List} of class names to export. - * - * @return @this - */ - @SuppressWarnings({"unchecked"}) - public R setExportOnlyTheseClasses(List exportOnlyTheseClasses) { - this.exportOnlyTheseClasses = exportOnlyTheseClasses; - return (R)this; - } - - public Map, Class> getDomainTypeMappings() { - return domainTypeMappings; - } - - @SuppressWarnings({"unchecked"}) - public R setDomainTypeMappings(Map, Class> domainTypeMappings) { - this.domainTypeMappings = domainTypeMappings; - return (R)this; - } - - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - @SuppressWarnings({"unchecked"}) - @Override public void afterPropertiesSet() throws Exception { - } - - /** - * Get the list of Repository names being exported. - * - * @return {@link List} of class names to export. - */ - public Set repositoryNames() { - refresh(); - return repositoryMetadata.keySet(); - } - - /** - * Is a Repository being exporter that supports this domain type? - * - * @param domainType - * Type of the domain class. - * - * @return {@literal true} if a Repository is being exported, {@literal false} otherwise. - */ - public boolean hasRepositoryFor(Class domainType) { - refresh(); - for(M repoMeta : repositoryMetadata.values()) { - if(repoMeta.domainType().isAssignableFrom(domainType)) { - return true; - } - } - return false; - } - - /** - * Get the RepositoryMetadata for the Repository responsible for this domain type. - * - * @param domainType - * Type of the domain class. - * - * @return {@link RepositoryMetadata} instance - */ - public M repositoryMetadataFor(Class domainType) { - refresh(); - // Look for an exact match - for(M repoMeta : repositoryMetadata.values()) { - if(repoMeta.domainType() == domainType) { - return repoMeta; - } - } - // Didn't find an exact match, look for domain type mapping - Class repoClass = domainTypeMappings.get(domainType); - if(null != repoClass) { - for(M repoMeta : repositoryMetadata.values()) { - if(repoMeta.repositoryClass() == repoClass) { - return repoMeta; - } - } - } - // Didn't find a mapping, look for a superclass - for(M repoMeta : repositoryMetadata.values()) { - if(repoMeta.domainType().isAssignableFrom(domainType)) { - return repoMeta; - } - } - return null; - } - - /** - * Get the {@link RepositoryMetadata} for the Repository exported under the given name. - * - * @param name - * Name a Repository would be exported under. - * - * @return {@link RepositoryMetadata} instance - */ - public M repositoryMetadataFor(String name) { - refresh(); - return repositoryMetadata.get(name); - } - - protected abstract M createRepositoryMetadata(String name, - Class domainType, - Class repoClass, - Repositories repositories); - - @SuppressWarnings({"unchecked"}) - public void refresh() { - if(null != repositories) { - return; - } - repositories = new Repositories(applicationContext); - repositoryMetadata = new HashMap(); - for(Class domainType : repositories) { - if(exportOnlyTheseClasses.isEmpty() || exportOnlyTheseClasses.contains(domainType.getName())) { - Class repoClass = repositories.getRepositoryInformationFor(domainType).getRepositoryInterface(); - String name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", "")); - RestResource resourceAnno = repoClass.getAnnotation(RestResource.class); - boolean exported = true; - if(null != resourceAnno) { - if(StringUtils.hasText(resourceAnno.path())) { - name = resourceAnno.path(); - } - exported = resourceAnno.exported(); - } - if(exported) { - repositoryMetadata.put(name, createRepositoryMetadata(name, domainType, repoClass, repositories)); - } - } - } - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java deleted file mode 100644 index 1af03a408..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; - -/** - * Abstract class used as a helper for those classes that need access to the exported repositories. - * - * @author Jon Brisbin - */ -public abstract class RepositoryExporterSupport> { - - protected List repositoryExporters = Collections.emptyList(); - - /** - * Get a List of {@link RepositoryExporter}s. - * - * @return Exported {@link RepositoryExporter}s. - */ - public List getRepositoryExporters() { - return repositoryExporters; - } - - /** - * Set the List of {@link RepositoryExporter}s. - * - * @param repositoryExporters - * Export this {@link List} of {@link RepositoryExporter}s. - */ - @Autowired(required = false) - public void setRepositoryExporters(List repositoryExporters) { - this.repositoryExporters = repositoryExporters; - } - - /** - * Get a List of {@link RepositoryExporter}s. - * - * @return Exported {@link RepositoryExporter}s. - */ - public List repositoryExporters() { - return repositoryExporters; - } - - /** - * Set the List of {@link RepositoryExporter}s. - * - * @param repositoryExporters - * Export this {@link List} of {@link RepositoryExporter}s. - * - * @return @this - */ - @SuppressWarnings({"unchecked"}) - public S repositoryExporters(List repositoryExporters) { - setRepositoryExporters(repositoryExporters); - return (S)this; - } - - /** - * Set the {@link RepositoryExporter}s to use. - * - * @param repositoryExporter - * - * @return - */ - @SuppressWarnings({"unchecked"}) - public S repositoryExporters(RepositoryExporter... repositoryExporter) { - setRepositoryExporters(Arrays.asList(repositoryExporter)); - return (S)this; - } - - /** - * Does a Repository exist for this name? - * - * @param name - * - * @return true - */ - public boolean hasRepositoryMetadataFor(String name) { - try { - return (null != repositoryMetadataFor(name)); - } catch(RepositoryNotFoundException ignored) { - return false; - } - } - - /** - * Is there a Repository responsible for this domain type? - * - * @param domainType - * - * @return - */ - public boolean hasRepositoryMetadataFor(Class domainType) { - try { - return (null != repositoryMetadataFor(domainType)); - } catch(RepositoryNotFoundException ignored) { - return false; - } - } - - /** - * Find {@link RepositoryMetadata} for the {@link org.springframework.data.repository.Repository} exported under this - * name. - * - * @param name - * URL segment name. - * - * @return {@link RepositoryMetadata} or {@literal null} if none found. - */ - @SuppressWarnings({"unchecked"}) - protected RepositoryMetadata repositoryMetadataFor(String name) { - for(RepositoryExporter exporter : repositoryExporters) { - RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(name); - if(null != repoMeta) { - return repoMeta; - } - } - throw new RepositoryNotFoundException("No repository found for name " + name); - } - - /** - * Find the {@link RepositoryMetadata} for the {@link org.springframework.data.repository.Repository} responsible for - * the given domain type. - * - * @param domainType - * Type of the domain class. - * - * @return {@link RepositoryMetadata} or {@literal null} if none found. - */ - @SuppressWarnings({"unchecked"}) - protected RepositoryMetadata repositoryMetadataFor(Class domainType) { - for(RepositoryExporter exporter : repositoryExporters) { - RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(domainType); - if(null != repoMeta) { - return repoMeta; - } - } - throw new RepositoryNotFoundException("No repository found for type " + domainType.getName()); - } - - /** - * Find the {@link RepositoryMetadata} for an attribute of an entity which is possibly managed by a {@link - * org.springframework.data.repository.Repository}. - * - * @param attrMeta - * {@link AttributeMetadata} of a possibly-managed entity. - * - * @return {@link RepositoryMetadata} or {@literal null} if none found. - */ - @SuppressWarnings({"unchecked"}) - protected RepositoryMetadata repositoryMetadataFor(AttributeMetadata attrMeta) { - if(null != attrMeta.elementType()) { - return repositoryMetadataFor(attrMeta.elementType()); - } else { - return repositoryMetadataFor(attrMeta.type()); - } - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadata.java deleted file mode 100644 index c941c3d6b..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadata.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.io.Serializable; -import java.lang.reflect.Method; -import java.util.Map; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.Repository; -import org.springframework.data.rest.repository.invoke.CrudMethod; -import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; - -/** - * Encapsulates necessary metadata about a {@link Repository}. - * - * @author Jon Brisbin - */ -public interface RepositoryMetadata> { - - /** - * The name this {@link Repository} is exported under. - * - * @return Name used in the URL for this Repository. - */ - String name(); - - /** - * Get the string value to be used as part of a link {@literal rel} attribute. - * - * @return Rel value used in links. - */ - String rel(); - - /** - * The type of domain object this {@link Repository} is repsonsible for. - * - * @return Type of the domain class. - */ - Class domainType(); - - /** - * The Class of the {@link Repository} subinterface. - * - * @return Type of the Repository being proxied. - */ - Class repositoryClass(); - - /** - * The {@link Repository} instance. - * - * @return The actual {@link Repository} instance. - */ - CrudRepository repository(); - - /** - * The {@link EntityMetadata} associated with the domain type of this {@literal Repository}. - * - * @return EntityMetadata associated with this Repository's domain type. - */ - E entityMetadata(); - - /** - * Get a {@link org.springframework.data.rest.repository.invoke.RepositoryQueryMethod} by key. - * - * @param key - * Segment of the URL to find a query method for. - * - * @return Found {@link org.springframework.data.rest.repository.invoke.RepositoryQueryMethod} or {@literal null} if - * none found. - */ - RepositoryQueryMethod queryMethod(String key); - - /** - * Get a Map of all {@link RepositoryQueryMethod}s, keyed by name. - * - * @return All query methods for this Repository. - */ - Map queryMethods(); - - /** - * Does this Repository all this method to be exported? - * - * @param method - * - * @return - */ - Boolean exportsMethod(CrudMethod method); - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryNotFoundException.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryNotFoundException.java deleted file mode 100644 index c1fec5151..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryNotFoundException.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.springframework.data.rest.repository; - -import org.springframework.dao.DataAccessResourceFailureException; - -/** - * @author Jon Brisbin - */ -public class RepositoryNotFoundException - extends DataAccessResourceFailureException { - - public RepositoryNotFoundException(String msg) { - super(msg); - } - - public RepositoryNotFoundException(String msg, Throwable cause) { - super(msg, cause); - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java new file mode 100644 index 000000000..7beab0f41 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java @@ -0,0 +1,72 @@ +package org.springframework.data.rest.repository; + +import java.net.URI; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.rest.repository.support.RepositoryInformationSupport; + +/** + * A {@link ConditionalGenericConverter} that can convert a {@link URI} domain entity. + * + * @author Jon Brisbin + */ +public class UriDomainClassConverter + extends RepositoryInformationSupport + implements ConditionalGenericConverter, + InitializingBean { + + private static TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); + + @Autowired + private DomainClassConverter domainClassConverter; + private Set convertiblePairs = new HashSet(); + + @Override public void afterPropertiesSet() throws Exception { + for(Class domainType : repositories) { + convertiblePairs.add(new ConvertiblePair(URI.class, domainType)); + } + } + + @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return URI.class.isAssignableFrom(sourceType.getType()) + && (null != repositories.getPersistentEntity(targetType.getType())); + } + + @Override public Set getConvertibleTypes() { + return convertiblePairs; + } + + @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + PersistentEntity entity = repositories.getPersistentEntity(targetType.getType()); + if(null == entity || !domainClassConverter.matches(STRING_TYPE, targetType)) { + throw new ConversionFailedException( + sourceType, + targetType, + source, + new IllegalArgumentException("No PersistentEntity information available for " + targetType.getType()) + ); + } + + URI uri = (URI)source; + String[] parts = uri.getPath().split("/"); + if(parts.length < 2) { + throw new ConversionFailedException( + sourceType, + targetType, + source, + new IllegalArgumentException("Cannot resolve URI " + uri + ". Is it local or remote? Only local URIs are resolvable.") + ); + } + + return domainClassConverter.convert(parts[parts.length - 1], STRING_TYPE, targetType); + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectUriResolver.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectUriResolver.java deleted file mode 100644 index f155a36f2..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectUriResolver.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.io.Serializable; -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.Stack; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.rest.core.UriResolver; -import org.springframework.data.rest.core.util.UriUtils; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.util.ClassUtils; - -/** - * @author Jon Brisbin - */ -public class UriToDomainObjectUriResolver - extends RepositoryExporterSupport - implements UriResolver { - - @Autowired(required = false) - private List conversionServices = Arrays.asList(new DefaultFormattingConversionService()); - - public List getConversionServices() { - return conversionServices; - } - - public UriToDomainObjectUriResolver setConversionServices(List conversionServices) { - this.conversionServices = conversionServices; - return this; - } - - @SuppressWarnings({"unchecked"}) - @Override public Object resolve(URI baseUri, URI uri) { - URI relativeUri = baseUri.relativize(uri); - Stack uris = UriUtils.explode(baseUri, relativeUri); - - if(uris.size() < 1) { - return null; - } - - String repoName = UriUtils.path(uris.get(0)); - String sId = UriUtils.path(uris.get(1)); - - RepositoryMetadata repoMeta = repositoryMetadataFor(repoName); - - CrudRepository repo; - if(null == (repo = repoMeta.repository())) { - return null; - } - - EntityMetadata entityMeta; - if(null == (entityMeta = repoMeta.entityMetadata())) { - return null; - } - - Class idType = (Class)entityMeta.idAttribute().type(); - Serializable serId = null; - if(ClassUtils.isAssignable(idType, String.class)) { - serId = sId; - } else { - for(ConversionService cs : conversionServices) { - if(cs.canConvert(String.class, idType)) { - serId = cs.convert(sId, idType); - break; - } - } - } - - return repo.findOne(serId); - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java index d380d2b9e..6366e433d 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java @@ -1,8 +1,14 @@ package org.springframework.data.rest.repository; +import static org.springframework.util.ReflectionUtils.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.validation.AbstractErrors; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; @@ -15,16 +21,16 @@ import org.springframework.validation.ObjectError; */ public class ValidationErrors extends AbstractErrors { - private String name; - private Object entity; - private EntityMetadata entityMetadata; + private String name; + private Object entity; + private PersistentEntity persistentEntity; private List globalErrors = new ArrayList(); private List fieldErrors = new ArrayList(); - public ValidationErrors(String name, Object entity, EntityMetadata entityMetadata) { + public ValidationErrors(String name, Object entity, PersistentEntity persistentEntity) { this.name = name; this.entity = entity; - this.entityMetadata = entityMetadata; + this.persistentEntity = persistentEntity; } @Override public String getObjectName() { @@ -58,6 +64,21 @@ public class ValidationErrors extends AbstractErrors { } @Override public Object getFieldValue(String field) { - return entityMetadata.attribute(field).get(entity); + PersistentProperty prop = (null != persistentEntity ? persistentEntity.getPersistentProperty(field) : null); + if(null == prop) { + return null; + } + + Method getter = prop.getGetter(); + if(null != getter) { + return invokeMethod(getter, entity); + } + Field fld = prop.getField(); + if(null != fld) { + return getField(fld, entity); + } + + return null; } + } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/Description.java similarity index 64% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java rename to spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/Description.java index c1db22198..d78c6f4f0 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/Description.java @@ -1,7 +1,6 @@ package org.springframework.data.rest.repository.annotation; import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -9,11 +8,12 @@ import java.lang.annotation.Target; /** * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.FIELD, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface HandleBeforeRenderResource { - - Class[] value() default {}; - +public @interface Description { + String value(); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java index a2c3f6b0b..b484f5579 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterDelete.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal afterDelete} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleAfterDelete { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkDelete.java index 68780cbbf..e8f22c458 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkDelete.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkDelete.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** + * Denotes a component that should handle the {@literal afterLinkDelete} event. + * * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleAfterLinkDelete { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkSave.java index 8f43d2e60..dcc73e21c 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkSave.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterLinkSave.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal afterLinkSave} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleAfterLinkSave { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java index b11c5f5a2..467de4827 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleAfterSave.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal afterSave} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleAfterSave { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java index d3f0b5610..be175c323 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeDelete.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal beforeDelete} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleBeforeDelete { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkDelete.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkDelete.java index 15e3b6e0c..eeb8edf2c 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkDelete.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkDelete.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** + * Denotes a component that should handle the {@literal beforeLinkDelete} event. + * * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleBeforeLinkDelete { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkSave.java index e65ff0acd..705151d63 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkSave.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeLinkSave.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal beforeLinkSave} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleBeforeLinkSave { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java deleted file mode 100644 index 1348d9ab3..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.springframework.data.rest.repository.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author Jon Brisbin - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface HandleBeforeRenderResources { - - Class[] value() default {}; - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java index b44029e2e..20a419b27 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeSave.java @@ -7,9 +7,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * @author Jon Brisbin + * Denotes a component that should handle the {@literal beforeSave} event. + * + * @author Jon Brisbin */ -@Target({ElementType.METHOD}) +@Target({ + ElementType.TYPE, + ElementType.METHOD + }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface HandleBeforeSave { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RestResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RestResource.java index c34b990e7..303d5cad4 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RestResource.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/RestResource.java @@ -10,7 +10,7 @@ import java.lang.annotation.Target; * Annotate a {@link org.springframework.data.repository.Repository} with this to influence how it is exported and what * the value of the {@literal rel} attribute will be in links. * - * @author Jon Brisbin + * @author Jon Brisbin */ @Target({ ElementType.FIELD, @@ -21,10 +21,25 @@ import java.lang.annotation.Target; @Inherited public @interface RestResource { + /** + * Flag indicating whether this resource is exported at all. + * + * @return {@literal true} if the resource is to be exported, {@literal false} otherwise. + */ boolean exported() default true; + /** + * The path segment under which this resource is to be exported. + * + * @return A valid path segment. + */ String path() default ""; + /** + * The rel value to use when generating links to this resource. + * + * @return A valid rel value. + */ String rel() default ""; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java index 75945d0bc..6b91786c4 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java @@ -1,55 +1,51 @@ package org.springframework.data.rest.repository.context; -import java.util.List; +import static org.springframework.core.GenericTypeResolver.*; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; -import org.springframework.data.rest.repository.RepositoryExporter; -import org.springframework.data.rest.repository.RepositoryExporterSupport; /** * Abstract class that listens for generic {@link RepositoryEvent}s and dispatches them to a specific * method based on the event type. * - * @author Jon Brisbin + * @author Jon Brisbin */ -public abstract class AbstractRepositoryEventListener> - extends RepositoryExporterSupport - implements ApplicationListener, - ApplicationContextAware { +public abstract class AbstractRepositoryEventListener implements ApplicationListener, + ApplicationContextAware { + private final Class INTERESTED_TYPE = resolveTypeArgument(getClass(), AbstractRepositoryEventListener.class); protected ApplicationContext applicationContext; - @Override public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } - @Autowired - public void setRepositoryExporters(List repositoryExporters) { - super.setRepositoryExporters(repositoryExporters); - } - + @SuppressWarnings({"unchecked"}) @Override public final void onApplicationEvent(RepositoryEvent event) { + Class srcType = event.getSource().getClass(); + if(null != INTERESTED_TYPE && !INTERESTED_TYPE.isAssignableFrom(srcType)) { + return; + } + if(event instanceof BeforeSaveEvent) { - onBeforeSave(event.getSource()); + onBeforeSave((T)event.getSource()); } else if(event instanceof AfterSaveEvent) { - onAfterSave(event.getSource()); + onAfterSave((T)event.getSource()); } else if(event instanceof BeforeLinkSaveEvent) { - onBeforeLinkSave(event.getSource(), ((BeforeLinkSaveEvent)event).getLinked()); + onBeforeLinkSave((T)event.getSource(), ((BeforeLinkSaveEvent)event).getLinked()); } else if(event instanceof AfterLinkSaveEvent) { - onAfterLinkSave(event.getSource(), ((AfterLinkSaveEvent)event).getLinked()); + onAfterLinkSave((T)event.getSource(), ((AfterLinkSaveEvent)event).getLinked()); } else if(event instanceof BeforeLinkDeleteEvent) { - onBeforeLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked()); + onBeforeLinkDelete((T)event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked()); } else if(event instanceof AfterLinkDeleteEvent) { - onAfterLinkDelete(event.getSource(), ((AfterLinkDeleteEvent)event).getLinked()); + onAfterLinkDelete((T)event.getSource(), ((AfterLinkDeleteEvent)event).getLinked()); } else if(event instanceof BeforeDeleteEvent) { - onBeforeDelete(event.getSource()); + onBeforeDelete((T)event.getSource()); } else if(event instanceof AfterDeleteEvent) { - onAfterDelete(event.getSource()); + onAfterDelete((T)event.getSource()); } } @@ -57,68 +53,80 @@ public abstract class AbstractRepositoryEventListener type : targetTypes) { EventHandlerMethod m = new EventHandlerMethod(type, handler, method); - if(LOG.isInfoEnabled()) { - LOG.info("Annotated handler method found: " + m); + if(LOG.isDebugEnabled()) { + LOG.debug("Annotated handler method found: " + m); } handlerMethods.put(eventType, m); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java index a5ff5be32..ddcfa2408 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java @@ -5,8 +5,7 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class BeforeSaveEvent - extends RepositoryEvent { +public class BeforeSaveEvent extends RepositoryEvent { public BeforeSaveEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ExceptionEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ExceptionEvent.java new file mode 100644 index 000000000..df1be1251 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ExceptionEvent.java @@ -0,0 +1,21 @@ +package org.springframework.data.rest.repository.context; + +/** + * An event to encapsulate an exception occurring anywhere within the REST exporter. + * + * @author Jon Brisbin + */ +public class ExceptionEvent extends RepositoryEvent { + public ExceptionEvent(Throwable t) { + super(t); + } + + /** + * Get the source of this exception event. + * + * @return The {@link Throwable} that is the source of this exception event. + */ + public Throwable getException() { + return (Throwable)getSource(); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java new file mode 100644 index 000000000..4bd91f058 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java @@ -0,0 +1,33 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.repository.support.Repositories; + +/** + * @author Jon Brisbin + */ +public class RepositoriesFactoryBean implements FactoryBean, + ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override public Repositories getObject() throws Exception { + return new Repositories(applicationContext); + } + + @Override public Class getObjectType() { + return Repositories.class; + } + + @Override public boolean isSingleton() { + return false; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java index 0e168f214..3b4fb1af3 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java @@ -1,16 +1,32 @@ package org.springframework.data.rest.repository.context; +import static org.springframework.beans.factory.BeanFactoryUtils.*; +import static org.springframework.core.annotation.AnnotationUtils.*; +import static org.springframework.util.StringUtils.*; + +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.repository.RepositoryConstraintViolationException; import org.springframework.data.rest.repository.ValidationErrors; +import org.springframework.data.rest.repository.annotation.HandleAfterDelete; +import org.springframework.data.rest.repository.annotation.HandleAfterLinkDelete; +import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave; +import org.springframework.data.rest.repository.annotation.HandleAfterSave; +import org.springframework.data.rest.repository.annotation.HandleBeforeDelete; +import org.springframework.data.rest.repository.annotation.HandleBeforeLinkDelete; +import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave; +import org.springframework.data.rest.repository.annotation.HandleBeforeSave; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; @@ -22,19 +38,31 @@ import org.springframework.validation.Validator; * @author Jon Brisbin */ public class ValidatingRepositoryEventListener - extends AbstractRepositoryEventListener + extends AbstractRepositoryEventListener implements InitializingBean { private static final Logger LOG = LoggerFactory.getLogger(ValidatingRepositoryEventListener.class); + @SuppressWarnings({"unchecked"}) + private static final List> ANNOTATIONS_TO_FIND = Arrays.asList( + HandleBeforeSave.class, + HandleAfterSave.class, + HandleBeforeDelete.class, + HandleAfterDelete.class, + HandleBeforeLinkSave.class, + HandleAfterLinkSave.class, + HandleBeforeLinkDelete.class, + HandleAfterLinkDelete.class + ); + + @Autowired + private Repositories repositories; private Multimap validators = ArrayListMultimap.create(); - @Override public void afterPropertiesSet() - throws Exception { + @Override public void afterPropertiesSet() throws Exception { if(validators.size() == 0) { - for(Map.Entry entry : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, - Validator.class) - .entrySet()) { + for(Map.Entry entry : beansOfTypeIncludingAncestors(applicationContext, + Validator.class).entrySet()) { String name = null; Validator v = entry.getValue(); @@ -42,7 +70,15 @@ public class ValidatingRepositoryEventListener name = entry.getKey().substring(0, entry.getKey().indexOf("Save") + 4); } else if(entry.getKey().contains("Delete")) { name = entry.getKey().substring(0, entry.getKey().indexOf("Delete") + 6); + } else { + Annotation anno; + for(Class annoType : ANNOTATIONS_TO_FIND) { + if(null != (anno = findAnnotation(v.getClass(), annoType))) { + name = uncapitalize(annoType.getSimpleName().substring(6)); + } + } } + if(null != name) { this.validators.put(name, v); } @@ -119,7 +155,7 @@ public class ValidatingRepositoryEventListener Class domainType = o.getClass(); errors = new ValidationErrors(domainType.getSimpleName(), o, - repositoryMetadataFor(domainType).entityMetadata()); + repositories.getPersistentEntity(domainType)); Collection validators = this.validators.get(event); if(null != validators) { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudMethod.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudMethod.java index b7ea7bf5b..94de6d692 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudMethod.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudMethod.java @@ -21,12 +21,13 @@ public enum CrudMethod { SAVE_SOME; /** - * Get an enum from a {@link Method}. Narrow down overriden methods by looking for {@link Iterable} in the first + * Get an enum from a {@link Method}. Narrow down overridden methods by looking for {@link Iterable} in the first * parameter, which tells us it is a '_SOME' type. * * @param m + * The CRUD method from the repository interface. * - * @return + * @return An enum representing which CRUD operation this method represents. */ public static CrudMethod fromMethod(Method m) { String s = m.getName(); @@ -52,7 +53,7 @@ public enum CrudMethod { /** * Turn this enum into a method name. * - * @return + * @return The method name as a string. */ public String toMethodName() { switch(this) { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java index fdf1093f7..5de1fc153 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java @@ -2,38 +2,38 @@ package org.springframework.data.rest.repository.invoke; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.repository.support.Methods; /** + * An abstraction to encapsulate metadata about a repository method. + * * @author Jon Brisbin */ public class RepositoryMethod { - private Method method; - private Class[] paramTypes; - private String[] paramNames; - private boolean pageable = false; - private boolean sortable = false; + private Method method; + private List methodParameters = new ArrayList(); + private List paramNames = new ArrayList(); + private boolean pageable = false; + private boolean sortable = false; public RepositoryMethod(Method method) { this.method = method; - paramTypes = method.getParameterTypes(); - for(Class type : paramTypes) { - if(Pageable.class.isAssignableFrom(type)) { - pageable = true; - } - if(Sort.class.isAssignableFrom(type)) { - sortable = true; - } - } - paramNames = Methods.NAME_DISCOVERER.getParameterNames(method); + + Class[] paramTypes = method.getParameterTypes(); + String[] paramNames = Methods.NAME_DISCOVERER.getParameterNames(method); if(null == paramNames) { paramNames = new String[paramTypes.length]; } + Annotation[][] paramAnnos = method.getParameterAnnotations(); for(int i = 0; i < paramAnnos.length; i++) { if(paramAnnos[i].length > 0) { @@ -49,25 +49,65 @@ public class RepositoryMethod { paramNames[i] = "arg" + i; } } + + int idx = 0; + for(Class type : paramTypes) { + if(Pageable.class.isAssignableFrom(type)) { + pageable = true; + } + if(Sort.class.isAssignableFrom(type)) { + sortable = true; + } + methodParameters.add(new MethodParameter(method, idx)); + idx++; + } + + Collections.addAll(this.paramNames, paramNames); } - public Class[] paramTypes() { - return paramTypes; + /** + * Get the method parameter types. + * + * @return Array of parameter types. + */ + public List getParameters() { + return methodParameters; } - public String[] paramNames() { + /** + * Get the method parameter names. + * + * @return Array of parameter names. + */ + public List getParameterNames() { return paramNames; } - public Method method() { + /** + * Get the reflected {@link Method} to invoke. + * + * @return The {@link Method} to invoke. + */ + public Method getMethod() { return method; } - public boolean pageable() { + /** + * Flag denoting whether this repository method returns a {@link org.springframework.data.domain.Page} result or not. + * + * @return {@literal true} if this method returns a {@link org.springframework.data.domain.Page}, {@literal false} + * otherwise. + */ + public boolean isPageable() { return pageable; } - public boolean sortable() { + /** + * Flag denoting whether this repository method accepts sorting information. + * + * @return {@literal true} if this method accepts a {@link Sort}, {@literal false} otherwise. + */ + public boolean isSortable() { return sortable; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java new file mode 100644 index 000000000..c4964713c --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java @@ -0,0 +1,219 @@ +package org.springframework.data.rest.repository.invoke; + +import static org.springframework.util.ReflectionUtils.*; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.core.RepositoryInformation; + +/** + * @author Jon Brisbin + */ +public class RepositoryMethodInvoker implements PagingAndSortingRepository { + + private final Object repository; + private final Map queryMethods = new HashMap(); + private RepositoryMethod saveOne; + private RepositoryMethod saveSome; + private RepositoryMethod findOne; + private RepositoryMethod exists; + private RepositoryMethod findAll; + private RepositoryMethod findAllSorted; + private RepositoryMethod findAllPaged; + private RepositoryMethod findSome; + private RepositoryMethod count; + private RepositoryMethod deleteOne; + private RepositoryMethod deleteOneById; + private RepositoryMethod deleteSome; + private RepositoryMethod deleteAll; + + @SuppressWarnings({"unchecked"}) + public RepositoryMethodInvoker(Object repository, + RepositoryInformation repoInfo, + final PersistentEntity persistentEntity) { + this.repository = repository; + Class repoType = repoInfo.getRepositoryInterface(); + + doWithMethods(repoType, new MethodCallback() { + @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + String name = method.getName(); + int cardinality = method.getParameterTypes().length; + Class paramType = (cardinality == 1 ? method.getParameterTypes()[0] : null); + boolean someMethod = (null != paramType && Iterable.class.isAssignableFrom(paramType)); + boolean byIdMethod = (null != paramType && paramType == Serializable.class); + boolean sortable = (null != paramType && Sort.class.isAssignableFrom(paramType)); + boolean pageable = (null != paramType && Pageable.class.isAssignableFrom(paramType)); + RepositoryMethod repoMethod = new RepositoryMethod(method); + + if("save".equals(name) && someMethod) { + saveSome = repoMethod; + } else if("save".equals(name)) { + saveOne = repoMethod; + } else if("findOne".equals(name)) { + findOne = repoMethod; + } else if("exists".equals(name)) { + exists = repoMethod; + } else if("findAll".equals(name) && someMethod) { + findSome = repoMethod; + } else if("findAll".equals(name) && sortable) { + findAllSorted = repoMethod; + } else if("findAll".equals(name) && pageable) { + findAllPaged = repoMethod; + } else if("findAll".equals(name)) { + findAll = repoMethod; + } else if("count".equals(name)) { + count = repoMethod; + } else if("delete".equals(name) && byIdMethod) { + deleteOneById = repoMethod; + } else if("delete".equals(name) && someMethod) { + deleteSome = repoMethod; + } else if("delete".equals(name)) { + deleteOne = repoMethod; + } else if("deleteAll".equals(name)) { + deleteAll = repoMethod; + } else { + queryMethods.put(name, repoMethod); + } + } + }); + } + + @SuppressWarnings({"unchecked"}) + @Override public S save(S entity) { + return (S)invokeMethod(saveOne.getMethod(), repository, entity); + } + + public boolean hasSaveOne() { + return null != saveOne; + } + + @SuppressWarnings({"unchecked"}) + @Override public Iterable save(Iterable entities) { + return (Iterable)invokeMethod(saveSome.getMethod(), repository, entities); + } + + public boolean hasSaveSome() { + return null != saveSome; + } + + @Override public Object findOne(Serializable serializable) { + return invokeMethod(findOne.getMethod(), repository, serializable); + } + + public boolean hasFindOne() { + return null != findOne; + } + + @Override public boolean exists(Serializable serializable) { + return (Boolean)invokeMethod(exists.getMethod(), repository, serializable); + } + + public boolean hasExists() { + return null != exists; + } + + @SuppressWarnings({"unchecked"}) + @Override public Iterable findAll() { + return (Iterable)invokeMethod(findAll.getMethod(), repository); + } + + public boolean hasFindAll() { + return null != findAll; + } + + @SuppressWarnings({"unchecked"}) + @Override public Iterable findAll(Iterable serializables) { + return (Iterable)invokeMethod(findSome.getMethod(), repository, serializables); + } + + public boolean hasFindSome() { + return null != findSome; + } + + @SuppressWarnings({"unchecked"}) + @Override public Iterable findAll(Sort sort) { + return (Iterable)invokeMethod(findAllSorted.getMethod(), repository, sort); + } + + public boolean hasFindAllSorted() { + return null != findAllSorted; + } + + @SuppressWarnings({"unchecked"}) + @Override public Page findAll(Pageable pageable) { + return (Page)invokeMethod(findAllPaged.getMethod(), repository, pageable); + } + + public boolean hasFindAllPageable() { + return null != findAllPaged; + } + + @Override public void delete(Serializable serializable) { + invokeMethod(deleteOneById.getMethod(), repository, serializable); + } + + public boolean hasDeleteOneById() { + return null != deleteOneById; + } + + @Override public long count() { + return (Long)invokeMethod(count.getMethod(), repository); + } + + public boolean hasCount() { + return null != count; + } + + @Override public void delete(Object entity) { + invokeMethod(deleteOne.getMethod(), repository, entity); + } + + public boolean hasDeleteOne() { + return null != deleteOne; + } + + @Override public void delete(Iterable entities) { + invokeMethod(deleteSome.getMethod(), repository, entities); + } + + public boolean hasDeleteSome() { + return null != deleteSome; + } + + @Override public void deleteAll() { + invokeMethod(deleteAll.getMethod(), repository); + } + + public boolean hasDeleteAll() { + return null != deleteAll; + } + + public Map getQueryMethods() { + return queryMethods; + } + + public RepositoryMethod getRepositoryMethod(String name) { + return queryMethods.get(name); + } + + public Object invokeQueryMethod(String name, Object... params) { + RepositoryMethod repoMethod = queryMethods.get(name); + if(null == repoMethod) { + throw new NoSuchMethodError(name); + } + return invokeMethod(repoMethod.getMethod(), repository, params); + } + + public Object invokeQueryMethod(RepositoryMethod method, Object... params) { + return invokeMethod(method.getMethod(), repository, params); + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java index 6220fb556..fd4c1e36f 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java @@ -5,7 +5,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import org.codehaus.jackson.annotate.JsonProperty; +import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.hateoas.Link; /** diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java deleted file mode 100644 index b83d1636a..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java +++ /dev/null @@ -1,180 +0,0 @@ -package org.springframework.data.rest.repository.jpa; - -import java.beans.PropertyDescriptor; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import javax.persistence.ManyToOne; -import javax.persistence.OneToOne; -import javax.persistence.metamodel.Attribute; -import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.MapAttribute; -import javax.persistence.metamodel.PluralAttribute; - -import org.springframework.beans.BeanUtils; -import org.springframework.data.rest.repository.AttributeMetadata; -import org.springframework.util.ReflectionUtils; - -/** - * Implementation of {@link AttributeMetadata} for JPA. - * - * @author Jon Brisbin - */ -public class JpaAttributeMetadata implements AttributeMetadata { - - private String name; - private Attribute attribute; - private Class type; - private Field field; - private Method getter; - private Method setter; - - public JpaAttributeMetadata(EntityType entityType, Attribute attribute) { - this.attribute = attribute; - name = attribute.getName(); - type = attribute.getJavaType(); - - field = ReflectionUtils.findField(entityType.getJavaType(), name); - ReflectionUtils.makeAccessible(field); - - PropertyDescriptor property = BeanUtils.getPropertyDescriptor(entityType.getJavaType(), name); - if(null != property) { - getter = property.getReadMethod(); - if(null != getter) { - ReflectionUtils.makeAccessible(getter); - } - - setter = property.getWriteMethod(); - if(null != setter) { - ReflectionUtils.makeAccessible(setter); - } - } - } - - @Override public String name() { - return name; - } - - @Override public Class type() { - return type; - } - - @Override public Class keyType() { - return (attribute instanceof MapAttribute - ? ((MapAttribute)attribute).getKeyJavaType() - : null); - } - - @Override public Class elementType() { - return (attribute instanceof PluralAttribute - ? ((PluralAttribute)attribute).getElementType().getJavaType() - : null); - } - - @Override public boolean isNullable() { - if(hasAnnotation(ManyToOne.class)) { - return annotation(ManyToOne.class).optional(); - } - - if(hasAnnotation(OneToOne.class)) { - return annotation(OneToOne.class).optional(); - } - - return true; - } - - @Override public boolean isCollectionLike() { - if(attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute)attribute; - switch(plattr.getCollectionType()) { - case COLLECTION: - case LIST: - return true; - default: - return false; - } - } else { - return false; - } - } - - @Override public Collection asCollection(Object target) { - return (Collection)get(target); - } - - @Override public boolean isSetLike() { - if(attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute)attribute; - switch(plattr.getCollectionType()) { - case SET: - return true; - default: - return false; - } - } else { - return false; - } - } - - @Override public Set asSet(Object target) { - return (Set)get(target); - } - - @Override public boolean isMapLike() { - if(attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute)attribute; - switch(plattr.getCollectionType()) { - case MAP: - return true; - default: - return false; - } - } else { - return false; - } - } - - @Override public Map asMap(Object target) { - return (Map)get(target); - } - - @Override public boolean hasAnnotation(Class annoType) { - return field.isAnnotationPresent(annoType); - } - - @Override public A annotation(Class annoType) { - return field.getAnnotation(annoType); - } - - @Override public Object get(Object target) { - if(null != getter) { - return ReflectionUtils.invokeMethod(getter, target); - } else { - return ReflectionUtils.getField(field, target); - } - } - - @Override public AttributeMetadata set(Object value, Object target) { - if(null != setter) { - ReflectionUtils.invokeMethod(setter, target, value); - } else { - ReflectionUtils.setField(field, target, value); - } - return this; - } - - @Override public String toString() { - return "JpaAttributeMetadata{" + - "name='" + name + '\'' + - ", attribute=" + attribute + - ", type=" + type + - ", field=" + field + - ", getter=" + getter + - ", setter=" + setter + - '}'; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java deleted file mode 100644 index 426bf5d81..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.springframework.data.rest.repository.jpa; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; -import javax.persistence.metamodel.Attribute; -import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.PluralAttribute; -import javax.persistence.metamodel.SingularAttribute; - -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.repository.EntityMetadata; -import org.springframework.data.rest.repository.annotation.RestResource; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * Implementation of {@link EntityMetadata} for JPA. - * - * @author Jon Brisbin - */ -public class JpaEntityMetadata implements EntityMetadata { - - private Class type; - private JpaAttributeMetadata idAttribute; - private JpaAttributeMetadata versionAttribute; - private Map embeddedAttributes = new HashMap(); - private Map linkedAttributes = new HashMap(); - - @SuppressWarnings({"unchecked"}) - public JpaEntityMetadata(Repositories repositories, EntityType entityType) { - type = entityType.getJavaType(); - idAttribute = new JpaAttributeMetadata(entityType, entityType.getId(entityType.getIdType().getJavaType())); - try { - if(null != entityType.getVersion(Long.class)) { - versionAttribute = new JpaAttributeMetadata(entityType, entityType.getVersion(Long.class)); - } - } catch(IllegalArgumentException ignored) { - // No version exists, just ignore it - } - - for(Attribute attr : entityType.getAttributes()) { - boolean exported = true; - Field field = ReflectionUtils.findField(type, attr.getJavaMember().getName()); - if(null == field) { - continue; - } - - RestResource fieldResourceAnno = field.getAnnotation(RestResource.class); - if(null != fieldResourceAnno) { - exported = fieldResourceAnno.exported(); - } - if(exported) { - String name = attr.getName(); - if(null != fieldResourceAnno && StringUtils.hasText(fieldResourceAnno.path())) { - name = fieldResourceAnno.path(); - } - Class attrType = (attr instanceof PluralAttribute - ? ((PluralAttribute)attr).getElementType().getJavaType() - : attr.getJavaType()); - if(repositories.hasRepositoryFor(attrType)) { - linkedAttributes.put(name, new JpaAttributeMetadata(entityType, attr)); - } else { - if((attr instanceof SingularAttribute && ((SingularAttribute)attr).isId())) { - // Don't export the id attribute - continue; - } else if(((attr instanceof SingularAttribute) && ((SingularAttribute)attr).isVersion()) - && (null == fieldResourceAnno || !StringUtils.hasText(fieldResourceAnno.path()))) { - // Don't export the version attribute - continue; - } - embeddedAttributes.put(name, new JpaAttributeMetadata(entityType, attr)); - } - } - } - } - - @Override public Class type() { - return type; - } - - @Override public Map embeddedAttributes() { - return embeddedAttributes; - } - - @Override public Map linkedAttributes() { - return linkedAttributes; - } - - @Override public JpaAttributeMetadata idAttribute() { - return idAttribute; - } - - @Override public JpaAttributeMetadata versionAttribute() { - return versionAttribute; - } - - @Override public JpaAttributeMetadata attribute(String name) { - if(idAttribute.name().equals(name)) { - return idAttribute; - } else if(null != versionAttribute && versionAttribute.name().equals(name)) { - return versionAttribute; - } else if(embeddedAttributes.containsKey(name)) { - return embeddedAttributes.get(name); - } else if(linkedAttributes.containsKey(name)) { - return linkedAttributes.get(name); - } - return null; - } - - @Override public String toString() { - return "JpaEntityMetadata{" + - "type=" + type + - ", idAttribute=" + idAttribute + - ", versionAttribute=" + versionAttribute + - ", embeddedAttributes=" + embeddedAttributes + - ", linkedAttributes=" + linkedAttributes + - '}'; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java deleted file mode 100644 index 89270fbd3..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.springframework.data.rest.repository.jpa; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.repository.RepositoryExporter; - -/** - * Implementation of {@link RepositoryExporter} for exporting JPA {@link Repository} subinterfaces. - * - * @author Jon Brisbin - */ -public class JpaRepositoryExporter - extends RepositoryExporter { - - protected EntityManager entityManager; - - @PersistenceContext - public void setEntityManager(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @SuppressWarnings({"unchecked"}) - @Override - protected JpaRepositoryMetadata createRepositoryMetadata(String name, Class domainType, Class repoClass, Repositories repositories) { - return new JpaRepositoryMetadata(name, domainType, repoClass, repositories, entityManager); - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java deleted file mode 100644 index 08d7b733e..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.springframework.data.rest.repository.jpa; - -import java.io.Serializable; -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import javax.persistence.EntityManager; -import javax.persistence.metamodel.Metamodel; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.core.EntityInformation; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.data.rest.repository.annotation.RestResource; -import org.springframework.data.rest.repository.invoke.CrudMethod; -import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; -import org.springframework.data.rest.repository.support.Methods; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * Implementation of {@link RepositoryMetadata} for JPA. - * - * @author Jon Brisbin - */ -public class JpaRepositoryMetadata implements RepositoryMetadata { - - private final String name; - private final Class repoClass; - private final CrudRepository repository; - private final EntityInformation entityInfo; - private final Map crudMethodExposed = new HashMap(); - private final Map queryMethods = new HashMap(); - - private String rel; - private JpaEntityMetadata entityMetadata; - - @SuppressWarnings({"unchecked"}) - public JpaRepositoryMetadata(String name, - Class domainType, - final Class repoClass, - Repositories repositories, - EntityManager entityManager) { - this.name = name; - this.repoClass = repoClass; - this.repository = repositories.getRepositoryFor(domainType); - this.entityInfo = repositories.getEntityInformationFor(domainType); - - RestResource resourceAnno = repoClass.getAnnotation(RestResource.class); - if(null != resourceAnno && StringUtils.hasText(resourceAnno.rel())) { - rel = resourceAnno.rel(); - } else { - rel = name; - } - - for(Method method : repositories.getRepositoryInformationFor(domainType).getQueryMethods()) { - String pathSeg = method.getName(); - RestResource methodResourceAnno = method.getAnnotation(RestResource.class); - boolean methodExported = true; - if(null != methodResourceAnno) { - if(StringUtils.hasText(methodResourceAnno.path())) { - pathSeg = methodResourceAnno.path(); - } - methodExported = methodResourceAnno.exported(); - } - if(methodExported) { - ReflectionUtils.makeAccessible(method); - queryMethods.put(pathSeg, new RepositoryQueryMethod(method)); - } - } - - ReflectionUtils.doWithMethods( - repoClass, - new ReflectionUtils.MethodCallback() { - @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - CrudMethod cr = CrudMethod.fromMethod(method); - RestResource rr = method.getAnnotation(RestResource.class); - if(null != rr) { - crudMethodExposed.put(cr, rr.exported()); - } - } - }, - new ReflectionUtils.MethodFilter() { - @Override public boolean matches(Method method) { - return (null != CrudMethod.fromMethod(method) && Methods.USER_METHODS.matches(method)); - } - } - ); - - Metamodel metamodel = entityManager.getMetamodel(); - entityMetadata = new JpaEntityMetadata(repositories, metamodel.entity(entityInfo.getJavaType())); - } - - @Override public String name() { - return name; - } - - @Override public String rel() { - return rel; - } - - @Override public Class domainType() { - return entityMetadata.type(); - } - - @Override public Class repositoryClass() { - return repoClass; - } - - @Override public CrudRepository repository() { - return repository; - } - - @Override public JpaEntityMetadata entityMetadata() { - return entityMetadata; - } - - @Override public RepositoryQueryMethod queryMethod(String key) { - return queryMethods.get(key); - } - - @Override public Map queryMethods() { - return Collections.unmodifiableMap(queryMethods); - } - - @Override public Boolean exportsMethod(CrudMethod method) { - Boolean b = crudMethodExposed.get(method); - if(null == b) { - return true; - } else { - return b; - } - } - - @Override public String toString() { - return "JpaRepositoryMetadata{" + - "name='" + name + '\'' + - ", repoClass=" + repoClass + - ", repository=" + repository + - ", entityInfo=" + entityInfo + - ", queryMethods=" + queryMethods + - ", entityMetadata=" + entityMetadata + - '}'; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java new file mode 100644 index 000000000..159137eb7 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java @@ -0,0 +1,95 @@ +package org.springframework.data.rest.repository.json; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.hateoas.Resource; + +/** + * @author Jon Brisbin + */ +public class JsonSchema extends Resource> { + + private final String name; + private final String description; + + public JsonSchema(String name, String description) { + super(new HashMap()); + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + @JsonProperty("properties") + @Override public Map getContent() { + return super.getContent(); + } + + public JsonSchema addProperty(String name, Property property) { + getContent().put(name, property); + return this; + } + + public boolean isArrayProperty(String name) { + return (getContent().containsKey(name) && getContent().get(name) instanceof ArrayProperty); + } + + public ArrayProperty getArrayProperty(String name) { + return (ArrayProperty)getContent().get(name); + } + + public static class Property { + private final String type; + private final String description; + private final boolean required; + + public Property(String type, String description, boolean required) { + this.type = type; + this.description = description; + this.required = required; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public boolean isRequired() { + return required; + } + } + + public static class ArrayProperty extends Property { + private List items = new ArrayList(); + + public ArrayProperty(String type, + String description, + boolean required) { + super(type, description, required); + } + + public List getItems() { + return items; + } + + public ArrayProperty setItems(List items) { + this.items = items; + return this; + } + + public

ArrayProperty addItem(P item) { + this.items.add(item); + return this; + } + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java new file mode 100644 index 000000000..391fc8649 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java @@ -0,0 +1,379 @@ +package org.springframework.data.rest.repository.json; + +import static org.springframework.beans.BeanUtils.*; +import static org.springframework.data.rest.core.util.UriUtils.*; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.data.rest.repository.UriDomainClassConverter; +import org.springframework.hateoas.Link; +import org.springframework.http.converter.HttpMessageNotReadableException; + +/** + * @author Jon Brisbin + */ +public class PersistentEntityJackson2Module extends SimpleModule implements InitializingBean { + + private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class); + private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class); + private final ConversionService conversionService; + @Autowired + private Repositories repositories; + @Autowired + private RepositoryRestConfiguration config; + @Autowired + private UriDomainClassConverter uriDomainClassConverter; + + public PersistentEntityJackson2Module(ConversionService conversionService) { + super(new Version(1, 1, 0, "BUILD-SNAPSHOT", "org.springframework.data.rest", "jackson-module")); + this.conversionService = conversionService; + + addSerializer(new ResourceSerializer()); + } + + public static boolean maybeAddAssociationLink(Repositories repositories, + RepositoryRestConfiguration config, + URI baseEntityUri, + ResourceMapping propertyMapping, + PersistentProperty persistentProperty, + List links) { + Class propertyType = persistentProperty.getType(); + if(persistentProperty.isCollectionLike() || persistentProperty.isArray()) { + propertyType = persistentProperty.getComponentType(); + } + + String propertyPath = (null != propertyMapping + ? propertyMapping.getPath() + : persistentProperty.getName()); + // In case a property mapping is specified but no path is set + if(null == propertyPath) { + propertyPath = persistentProperty.getName(); + } + // entityRel + "." + + String propertyRel = (null != propertyMapping + ? propertyMapping.getRel() + : propertyPath); + if(repositories.hasRepositoryFor(propertyType)) { + // This is a managed type, generate a Link + RepositoryInformation linkedRepoInfo = repositories.getRepositoryInformationFor(propertyType); + ResourceMapping linkedRepoMapping = getResourceMapping(config, linkedRepoInfo); + if(linkedRepoMapping.isExported()) { + URI uri = buildUri(baseEntityUri, propertyPath); + Link l = new Link(uri.toString(), propertyRel); + links.add(l); + // This is an association. We added a Link. + return true; + } + } + // This is not an association. No Link was added. + return false; + } + + @SuppressWarnings({"unchecked"}) + @Override public void afterPropertiesSet() throws Exception { + for(Class domainType : repositories) { + addDeserializer(domainType, new ResourceDeserializer(repositories.getPersistentEntity(domainType))); + } + } + + private class ResourceDeserializer extends StdDeserializer { + + private final PersistentEntity persistentEntity; + private final Object defaultObject; + private final Map defaultValues = new HashMap(); + + @SuppressWarnings({"unchecked"}) + private ResourceDeserializer(PersistentEntity persistentEntity) { + super(persistentEntity.getType()); + this.persistentEntity = persistentEntity; + this.defaultObject = instantiateClass(getValueClass()); + + final BeanWrapper wrapper = BeanWrapper.create(defaultObject, conversionService); + persistentEntity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty prop) { + Object defaultValue = wrapper.getProperty(prop); + if(null != defaultValue) { + defaultValues.put(prop.getName(), defaultValue); + } + } + }); + } + + @SuppressWarnings({"unchecked"}) + @Override public T deserialize(JsonParser jp, + DeserializationContext ctxt) throws IOException, + JsonProcessingException { + Object entity = instantiateClass(getValueClass()); + BeanWrapper wrapper = BeanWrapper.create(entity, conversionService); + ResourceMapping domainMapping = config.getResourceMappingForDomainType(getValueClass()); + + for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { + String name = jp.getCurrentName(); + switch(tok) { + case FIELD_NAME: { + if("href".equals(name)) { + URI uri = URI.create(jp.nextTextValue()); + TypeDescriptor entityType = TypeDescriptor.forObject(entity); + if(uriDomainClassConverter.matches(URI_TYPE, entityType)) { + entity = uriDomainClassConverter.convert(uri, URI_TYPE, entityType); + } + continue; + } + + if("rel".equals(name)) { + // rel is currently ignored + continue; + } + + PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(name); + if(null == persistentProperty) { + String errMsg = "Property '" + name + "' not found for entity " + getValueClass().getName(); + if(null == domainMapping) { + throw new HttpMessageNotReadableException(errMsg); + } + String propertyName = domainMapping.getNameForPath(name); + if(null == propertyName) { + throw new HttpMessageNotReadableException(errMsg); + } + persistentProperty = persistentEntity.getPersistentProperty(propertyName); + if(null == persistentProperty) { + throw new HttpMessageNotReadableException(errMsg); + } + } + + Object val = null; + + if("links".equals(name)) { + if((tok = jp.nextToken()) == JsonToken.START_ARRAY) { + while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { + // Advance past the links + } + } else if(tok == JsonToken.VALUE_NULL) { + // skip null value + } else { + throw new HttpMessageNotReadableException( + "Property 'links' is not of array type. Either eliminate this property from the document or make it an array."); + } + continue; + } + + if(null == persistentProperty) { + // do nothing + continue; + } + + // Try and read the value of this attribute. + // The method of doing that varies based on the type of the property. + if(persistentProperty.isCollectionLike()) { + Class ctype = (Class)persistentProperty.getType(); + Collection c = (Collection)wrapper.getProperty(persistentProperty, ctype, false); + if(null == c || c == Collections.EMPTY_LIST || c == Collections.EMPTY_SET) { + if(Collection.class.isAssignableFrom(ctype)) { + c = new ArrayList(); + } else if(Set.class.isAssignableFrom(ctype)) { + c = new HashSet(); + } + } + + if((tok = jp.nextToken()) == JsonToken.START_ARRAY) { + while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { + Object cval = jp.readValueAs(persistentProperty.getComponentType()); + c.add(cval); + } + + val = c; + } else if(tok == JsonToken.VALUE_NULL) { + val = null; + } else { + throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection."); + } + } else if(persistentProperty.isMap()) { + Class mtype = (Class)persistentProperty.getType(); + Map m = (Map)wrapper.getProperty(persistentProperty, mtype, false); + if(null == m || m == Collections.EMPTY_MAP) { + m = new HashMap(); + } + + if((tok = jp.nextToken()) == JsonToken.START_OBJECT) { + do { + name = jp.getCurrentName(); + // TODO resolve domain object from URI + tok = jp.nextToken(); + Object mval = jp.readValueAs(persistentProperty.getMapValueType()); + + m.put(name, mval); + } while((tok = jp.nextToken()) != JsonToken.END_OBJECT); + + val = m; + } else if(tok == JsonToken.VALUE_NULL) { + val = null; + } else { + throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map."); + } + } else { + if((tok = jp.nextToken()) != JsonToken.VALUE_NULL) { + val = jp.readValueAs(persistentProperty.getType()); + } + } + + if(null != val) { + Object defaultValue = defaultValues.get(persistentProperty.getName()); + if(null == defaultValue || defaultValue != val) { + wrapper.setProperty(persistentProperty, val, false); + } + } + + break; + } + } + } + + return (T)entity; + } + } + + private class ResourceSerializer extends StdSerializer { + + private ResourceSerializer() { + super(PersistentEntityResource.class); + } + + @SuppressWarnings({"unchecked"}) + @Override public void serialize(final PersistentEntityResource resource, + final JsonGenerator jgen, + final SerializerProvider provider) throws IOException, + JsonGenerationException { + if(LOG.isDebugEnabled()) { + LOG.debug("Serializing PersistentEntity " + resource.getPersistentEntity()); + } + + Object obj = resource.getContent(); + + final PersistentEntity persistentEntity = resource.getPersistentEntity(); + final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); + + final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(persistentEntity.getType()); + final ResourceMapping repoMapping = getResourceMapping(config, repoInfo); + + final BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); + final Object entityId = wrapper.getProperty(persistentEntity.getIdProperty()); + + final URI baseEntityUri = buildUri(resource.getBaseUri(), + repoMapping.getPath(), + entityId.toString()); + + final List links = new ArrayList(); + // Start with ResourceProcessor-added links + links.addAll(resource.getLinks()); + + jgen.writeStartObject(); + try { + persistentEntity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { + if(persistentProperty.isIdProperty() && !config.isIdExposedFor(persistentEntity.getType())) { + return; + } + ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); + if(null != propertyMapping && !propertyMapping.isExported()) { + return; + } + + if(persistentProperty.isEntity() && maybeAddAssociationLink(repositories, + config, + baseEntityUri, + propertyMapping, + persistentProperty, + links)) { + return; + } + + // Property is a normal or non-managed property. + String propertyName = (null != propertyMapping ? propertyMapping.getPath() : persistentProperty.getName()); + Object propertyValue = wrapper.getProperty(persistentProperty); + try { + jgen.writeObjectField(propertyName, propertyValue); + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + }); + + // Add associations as links + persistentEntity.doWithAssociations(new AssociationHandler() { + @Override public void doWithAssociation(Association association) { + PersistentProperty persistentProperty = association.getInverse(); + ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); + if(null != propertyMapping && !propertyMapping.isExported()) { + return; + } + if(maybeAddAssociationLink(repositories, + config, + baseEntityUri, + propertyMapping, + persistentProperty, + links)) { + return; + } + // Association Link was not added, probably because this isn't a managed type. Add value of property inline. + Object propertyValue = wrapper.getProperty(persistentProperty); + try { + jgen.writeObjectField(persistentProperty.getName(), propertyValue); + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + }); + + jgen.writeArrayFieldStart("links"); + for(Link l : links) { + jgen.writeObject(l); + } + jgen.writeEndArray(); + + } catch(IllegalStateException e) { + throw (IOException)e.getCause(); + } finally { + jgen.writeEndObject(); + } + } + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java new file mode 100644 index 000000000..8d20f6c63 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java @@ -0,0 +1,116 @@ +package org.springframework.data.rest.repository.json; + +import static org.springframework.data.rest.core.util.UriUtils.*; +import static org.springframework.data.rest.repository.json.PersistentEntityJackson2Module.*; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; +import static org.springframework.util.StringUtils.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.validation.constraints.NotNull; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.annotation.Description; +import org.springframework.data.rest.repository.support.RepositoryInformationSupport; +import org.springframework.hateoas.Link; + +/** + * @author Jon Brisbin + */ +public class PersistentEntityToJsonSchemaConverter + extends RepositoryInformationSupport + implements ConditionalGenericConverter, + InitializingBean { + + private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); + private static final TypeDescriptor SCHEMA_TYPE = TypeDescriptor.valueOf(JsonSchema.class); + private Set convertiblePairs = new HashSet(); + + @Override public void afterPropertiesSet() throws Exception { + for(Class domainType : repositories) { + convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class)); + } + } + + @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (Class.class.isAssignableFrom(sourceType.getType()) && JsonSchema.class.isAssignableFrom(targetType.getType())); + } + + @Override public Set getConvertibleTypes() { + return convertiblePairs; + } + + public JsonSchema convert(Class domainType) { + return (JsonSchema)convert(domainType, STRING_TYPE, SCHEMA_TYPE); + } + + @SuppressWarnings({"unchecked"}) + @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + PersistentEntity persistentEntity = repositories.getPersistentEntity((Class)source); + final ResourceMapping repoMapping = getResourceMapping(config, + repositories.getRepositoryInformationFor(persistentEntity.getType())); + final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); + final URI baseEntityUri = buildUri(config.getBaseUri(), repoMapping.getPath(), "{id}"); + String entityDesc = persistentEntity.getType().isAnnotationPresent(Description.class) + ? ((Description)persistentEntity.getType().getAnnotation(Description.class)).value() + : null; + + final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), entityDesc); + persistentEntity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { + Class propertyType = persistentProperty.getType(); + String type = uncapitalize(propertyType.getSimpleName()); + boolean notNull = (persistentProperty.getField().isAnnotationPresent(Nonnull.class) + || persistentProperty.getGetter().isAnnotationPresent(Nonnull.class)) + || (persistentProperty.getField().isAnnotationPresent(NotNull.class) + || persistentProperty.getGetter().isAnnotationPresent(NotNull.class)); + String desc = persistentProperty.getField().isAnnotationPresent(Description.class) + ? persistentProperty.getField().getAnnotation(Description.class).value() + : persistentProperty.getGetter().isAnnotationPresent(Description.class) + ? persistentProperty.getGetter().getAnnotation(Description.class).value() + : null; + + JsonSchema.Property property; + if(persistentProperty.isCollectionLike()) { + property = new JsonSchema.ArrayProperty("array", desc, notNull); + } else { + property = new JsonSchema.Property(type, desc, notNull); + } + jsonSchema.addProperty(persistentProperty.getName(), property); + } + }); + + final List links = new ArrayList(); + persistentEntity.doWithAssociations(new AssociationHandler() { + @Override public void doWithAssociation(Association association) { + PersistentProperty persistentProperty = association.getInverse(); + ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); + if(null != propertyMapping && !propertyMapping.isExported()) { + return; + } + maybeAddAssociationLink(repositories, + config, + baseEntityUri, + propertyMapping, + persistentProperty, + links); + } + }); + jsonSchema.add(links); + + return jsonSchema; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/DomainObjectMerger.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/DomainObjectMerger.java new file mode 100644 index 000000000..24dbed1e7 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/DomainObjectMerger.java @@ -0,0 +1,100 @@ +package org.springframework.data.rest.repository.support; + +import static org.springframework.beans.BeanUtils.*; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.support.Repositories; + +/** + * @author Jon Brisbin + */ +public class DomainObjectMerger { + + private final Map, PersistentEntity> entities = new ConcurrentHashMap, PersistentEntity>(); + private final Map defaultValues = new ConcurrentHashMap(); + private final Repositories repositories; + private final ConversionService conversionService; + + @Autowired + public DomainObjectMerger(Repositories repositories, + ConversionService conversionService) { + this.repositories = repositories; + this.conversionService = conversionService; + } + + @SuppressWarnings({"unchecked"}) + public void merge(Object from, Object target) { + if(null == from || null == target) { + return; + } + final BeanWrapper fromWrapper = BeanWrapper.create(from, conversionService); + final BeanWrapper targetWrapper = BeanWrapper.create(target, conversionService); + + PersistentEntity entity = getPerisistentEntity(target.getClass()); + Class clazz = entity.getType(); + final String clazzName = clazz.getSimpleName(); + + entity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { + String mapKey = clazzName + "." + persistentProperty.getName(); + Object fromVal = fromWrapper.getProperty(persistentProperty); + Object defaultVal = defaultValues.get(mapKey); + if(null != fromVal && !fromVal.equals(defaultVal)) { + targetWrapper.setProperty(persistentProperty, fromVal); + } + } + }); + + entity.doWithAssociations(new AssociationHandler() { + @Override public void doWithAssociation(Association association) { + PersistentProperty persistentProperty = association.getInverse(); + String mapKey = clazzName + "." + persistentProperty.getName(); + Object fromVal = fromWrapper.getProperty(persistentProperty); + Object defaultVal = defaultValues.get(mapKey); + if(null != fromVal && !fromVal.equals(defaultVal)) { + targetWrapper.setProperty(persistentProperty, fromVal); + } + } + }); + } + + @SuppressWarnings({"unchecked"}) + private PersistentEntity getPerisistentEntity(Class clazz) { + PersistentEntity entity = entities.get(clazz); + if(null == entity) { + entity = repositories.getPersistentEntity(clazz); + final String clazzName = clazz.getSimpleName(); + final BeanWrapper wrapper = BeanWrapper.create(instantiateClass(clazz), conversionService); + entity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { + Object val = wrapper.getProperty(persistentProperty); + if(null != val) { + defaultValues.put(clazzName + "." + persistentProperty.getName(), val); + } + } + }); + entity.doWithAssociations(new AssociationHandler() { + @Override public void doWithAssociation(Association association) { + PersistentProperty persistentProperty = association.getInverse(); + Object val = wrapper.getProperty(persistentProperty); + if(null != val) { + defaultValues.put(clazzName + "." + persistentProperty.getName(), val); + } + } + }); + entities.put(clazz, entity); + } + return entity; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryEntityLinks.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryEntityLinks.java new file mode 100644 index 000000000..e295a5370 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryEntityLinks.java @@ -0,0 +1,117 @@ +package org.springframework.data.rest.repository.support; + +import static org.springframework.data.rest.core.util.UriUtils.*; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.net.URI; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.core.AbstractEntityLinks; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +public class RepositoryEntityLinks extends AbstractEntityLinks { + + private final URI baseUri; + private final Repositories repositories; + private final RepositoryRestConfiguration config; + + public RepositoryEntityLinks(URI baseUri, + Repositories repositories, + RepositoryRestConfiguration config) { + this.baseUri = baseUri; + this.repositories = repositories; + this.config = config; + } + + @Override public boolean supports(Class delimiter) { + PersistentEntity persistentEntity = repositories.getPersistentEntity(delimiter); + return (null != persistentEntity); + } + + @Override public LinkBuilder linkFor(Class type) { + RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); + PersistentEntity persistentEntity = repositories.getPersistentEntity(type); + if(null == persistentEntity) { + throw new IllegalArgumentException(type + " is not managed by any repository."); + } + return new PersistentEntityLinkBuilder(baseUri, repoInfo, persistentEntity); + } + + @Override public LinkBuilder linkFor(Class type, Object... parameters) { + return linkFor(type); + } + + @Override public Link linkToCollectionResource(Class type) { + RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); + if(null == repoInfo) { + throw new IllegalArgumentException(type + " is not managed by any repository."); + } + ResourceMapping mapping = getResourceMapping(config, repoInfo); + return linkFor(type).withRel(mapping.getRel()); + } + + @Override public Link linkToSingleResource(Class type, Object id) { + RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); + if(null == repoInfo) { + throw new IllegalArgumentException(type + " is not managed by any repository."); + } + ResourceMapping repoMapping = getResourceMapping(config, repoInfo); + PersistentEntity persistentEntity = repositories.getPersistentEntity(type); + ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); + return linkFor(type).slash(id).withRel(repoMapping.getRel() + "." + entityMapping.getRel()); + } + + private class PersistentEntityLinkBuilder implements LinkBuilder { + private final UriComponentsBuilder builder; + private final ResourceMapping repoMapping; + private final ResourceMapping entityMapping; + + private PersistentEntityLinkBuilder(URI baseUri, + RepositoryInformation repoInfo, + PersistentEntity persistentEntity) { + this.repoMapping = getResourceMapping(config, repoInfo); + this.entityMapping = getResourceMapping(config, persistentEntity); + this.builder = UriComponentsBuilder.fromUri(buildUri(baseUri, repoMapping.getPath())); + } + + @Override public LinkBuilder slash(Object object) { + String path = String.format("%s", object); + if(object instanceof PersistentProperty) { + String propName = ((PersistentProperty)object).getName(); + if(entityMapping.hasResourceMappingFor(propName)) { + path = entityMapping.getResourceMappingFor(propName).getPath(); + } + } + builder.pathSegment(path); + return this; + } + + @Override public LinkBuilder slash(Identifiable identifiable) { + return slash(identifiable.getId()); + } + + @Override public URI toUri() { + return builder.build().toUri(); + } + + @Override public Link withRel(String rel) { + return new Link(builder.build().toUriString(), rel); + } + + @Override public Link withSelfRel() { + return withRel("self"); + } + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java new file mode 100644 index 000000000..3a66a4040 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java @@ -0,0 +1,76 @@ +package org.springframework.data.rest.repository.support; + +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; +import static org.springframework.util.ReflectionUtils.*; +import static org.springframework.util.StringUtils.*; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.invoke.RepositoryMethod; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * @author Jon Brisbin + */ +public abstract class RepositoryInformationSupport { + + protected Repositories repositories; + protected RepositoryRestConfiguration config; + protected MultiValueMap, RepositoryMethod> repositoryMethods = new LinkedMultiValueMap, RepositoryMethod>(); + + public Repositories getRepositories() { + return repositories; + } + + @Autowired + public void setRepositories(Repositories repositories) { + this.repositories = repositories; + for(Class domainType : repositories) { + final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType); + doWithMethods(repoInfo.getRepositoryInterface(), new MethodCallback() { + @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + repositoryMethods.add(repoInfo.getRepositoryInterface(), new RepositoryMethod(method)); + } + }); + } + } + + public RepositoryRestConfiguration getConfig() { + return config; + } + + @Autowired + public void setConfig(RepositoryRestConfiguration config) { + this.config = config; + } + + protected RepositoryInformation findRepositoryInfoFor(String pathSegment) { + if(!hasText(pathSegment)) { + return null; + } + for(Class domainType : repositories) { + RepositoryInformation repoInfo = findRepositoryInfoFor(domainType); + ResourceMapping mapping = getResourceMapping(config, repoInfo); + if(pathSegment.equals(mapping.getPath()) && mapping.isExported()) { + return repoInfo; + } + } + return null; + } + + protected RepositoryInformation findRepositoryInfoFor(Class domainType) { + PersistentEntity entity = repositories.getPersistentEntity(domainType); + if(null != entity) { + return repositories.getRepositoryInformationFor(domainType); + } + return null; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java new file mode 100644 index 000000000..47a35fd46 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java @@ -0,0 +1,130 @@ +package org.springframework.data.rest.repository.support; + +import static org.springframework.core.annotation.AnnotationUtils.*; +import static org.springframework.util.StringUtils.*; + +import java.lang.reflect.Method; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.annotation.RestResource; + +/** + * Helper methods to get the default rel and path values or to use values supplied by annotations. + * + * @author Jon Brisbin + */ +public abstract class ResourceMappingUtils { + + protected ResourceMappingUtils() { + } + + public static String findRel(Class type) { + RestResource anno; + if(null != (anno = findAnnotation(type, RestResource.class))) { + if(hasText(anno.rel())) { + return anno.rel(); + } + } + + return uncapitalize(type.getSimpleName().replaceAll("Repository", "")); + } + + public static String findRel(Method method) { + RestResource anno; + if(null != (anno = findAnnotation(method, RestResource.class))) { + if(hasText(anno.rel())) { + return anno.rel(); + } + } + + return method.getName(); + } + + public static String findPath(Class type) { + RestResource anno; + if(null != (anno = findAnnotation(type, RestResource.class))) { + if(hasText(anno.path())) { + return anno.path(); + } + } + + return uncapitalize(type.getSimpleName().replaceAll("Repository", "")); + } + + public static String findPath(Method method) { + RestResource anno; + if(null != (anno = findAnnotation(method, RestResource.class))) { + if(hasText(anno.path())) { + return anno.path(); + } + } + + return method.getName(); + } + + public static boolean findExported(Class type) { + RestResource anno; + return null == (anno = findAnnotation(type, RestResource.class)) || anno.exported(); + } + + public static boolean findExported(Method method) { + RestResource anno; + return null == (anno = findAnnotation(method, RestResource.class)) || anno.exported(); + } + + public static ResourceMapping getResourceMapping(RepositoryRestConfiguration config, + PersistentEntity persistentEntity) { + if(null == persistentEntity) { + return null; + } + Class domainType = persistentEntity.getType(); + ResourceMapping mapping = (null != config ? config.getResourceMappingForDomainType(domainType) : null); + return merge(domainType, mapping); + } + + public static ResourceMapping getResourceMapping(RepositoryRestConfiguration config, + RepositoryInformation repoInfo) { + if(null == repoInfo) { + return null; + } + Class repoType = repoInfo.getRepositoryInterface(); + ResourceMapping mapping = (null != config ? config.getResourceMappingForRepository(repoType) : null); + return merge(repoType, mapping); + } + + public static ResourceMapping merge(Method method, ResourceMapping mapping) { + ResourceMapping defaultMapping = new ResourceMapping( + findRel(method), + findPath(method), + findExported(method) + ); + if(null != mapping) { + return new ResourceMapping( + (null != mapping.getRel() ? mapping.getRel() : defaultMapping.getRel()), + (null != mapping.getPath() ? mapping.getPath() : defaultMapping.getPath()), + (mapping.isExported() != defaultMapping.isExported() ? mapping.isExported() : defaultMapping.isExported()) + ); + } + return defaultMapping; + } + + public static ResourceMapping merge(Class type, ResourceMapping mapping) { + ResourceMapping defaultMapping = new ResourceMapping( + findRel(type), + findPath(type), + findExported(type) + ); + if(null != mapping) { + return new ResourceMapping( + (null != mapping.getRel() ? mapping.getRel() : defaultMapping.getRel()), + (null != mapping.getPath() ? mapping.getPath() : defaultMapping.getPath()), + (mapping.isExported() != defaultMapping.isExported() ? mapping.isExported() : defaultMapping.isExported())) + .addResourceMappings(mapping.getResourceMappings()); + } + return defaultMapping; + } + +} diff --git a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy deleted file mode 100644 index 539e2383b..000000000 --- a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy +++ /dev/null @@ -1,101 +0,0 @@ -package org.springframework.data.rest.repository.spec - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.ApplicationContext -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.data.rest.repository.RepositoryExporter -import org.springframework.data.rest.repository.annotation.* -import org.springframework.data.rest.repository.context.* -import org.springframework.data.rest.repository.test.ApplicationConfig -import org.springframework.data.rest.repository.test.Person -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -/** - * @author Jon Brisbin - */ -@ContextConfiguration(classes = [ApplicationConfig, EventsApplicationConfig]) -class ExtensionsSpec extends Specification { - - @Autowired - ApplicationContext appCtx - @Autowired - PersonEventHandler handler - @Autowired - RepositoryExporter exporter - - def "responds to ApplicationEvents in annotated handlers"() { - - given: - def p = new Person("John Doe") - - when: - appCtx.publishEvent(new BeforeSaveEvent(p)) - appCtx.publishEvent(new AfterSaveEvent(p)) - appCtx.publishEvent(new BeforeLinkSaveEvent(p, new Object())) - appCtx.publishEvent(new AfterLinkSaveEvent(p, new Object())) - appCtx.publishEvent(new BeforeDeleteEvent(p)) - appCtx.publishEvent(new AfterDeleteEvent(p)) - - then: - handler.beforeSave - handler.afterSave - handler.beforeChildSave - handler.afterChildSave - handler.beforeDelete - handler.afterDelete - - } - -} - -@Configuration -class EventsApplicationConfig { - - @Bean AnnotatedHandlerBeanPostProcessor handlerBeanPostProcessor() { - return new AnnotatedHandlerBeanPostProcessor(); - } - - @Bean PersonEventHandler personEventHandler() { - new PersonEventHandler() - } - -} - -@RepositoryEventHandler(Person) -class PersonEventHandler { - - def beforeSave = false - def afterSave = false - def beforeChildSave = false - def afterChildSave = false - def beforeDelete = false - def afterDelete = false - - @HandleBeforeSave void handleBeforeSave(Person p) { - beforeSave = true - } - - @HandleAfterSave void handleAfterSave(Person p) { - afterSave = true - } - - @HandleBeforeLinkSave void handleBeforeChildSave(Person p, Object child) { - beforeChildSave = true - } - - @HandleAfterLinkSave void handleAfterChildSave(Person p, Object child) { - afterChildSave = true - } - - @HandleBeforeDelete void handleBeforeDelete(Person p) { - beforeDelete = true - } - - @HandleAfterDelete void handleAfterDelete(Person p) { - afterDelete = true - } - -} - diff --git a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/JpaMetadataSpec.groovy b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/JpaMetadataSpec.groovy deleted file mode 100644 index 0e967b849..000000000 --- a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/JpaMetadataSpec.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package org.springframework.data.rest.repository.spec - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.ApplicationContext -import org.springframework.data.rest.repository.RepositoryExporter -import org.springframework.data.rest.repository.RepositoryMetadata -import org.springframework.data.rest.repository.test.ApplicationConfig -import org.springframework.data.rest.repository.test.Family -import org.springframework.data.rest.repository.test.FamilyRepository -import org.springframework.data.rest.repository.test.Person -import org.springframework.data.rest.repository.test.PersonRepository -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -import javax.persistence.EntityManager -import javax.persistence.PersistenceContext - -/** - * @author Jon Brisbin - */ -@ContextConfiguration(classes = [ApplicationConfig]) -class JpaMetadataSpec extends Specification { - - @Autowired - ApplicationContext applicationContext - @PersistenceContext - EntityManager entityManager - @Autowired - List exporters - - RepositoryMetadata metadata(name) { - exporters.find { null != it.repositoryMetadataFor(name) }?.repositoryMetadataFor(name) - } - - def "finds repositories in ApplicationContext"() { - - when: "find repo by String identifier" - def repo = metadata("person")?.repository() - - then: - null != repo - repo instanceof PersonRepository - - when: "find repo by domain Class" - repo = metadata(Family)?.repository() - - then: - null != repo - repo instanceof FamilyRepository - - } - - def "provides entity metadata"() { - - given: - def personRepo = metadata(Person)?.repository() - def familyRepo = metadata(Family)?.repository() - def johnDoe = personRepo?.save(new Person("John Doe")) - def janeDoe = personRepo?.save(new Person("Jane Doe")) - def doeFamily = familyRepo?.save(new Family( - surname: "Doe", - members: [johnDoe, janeDoe] - )) - - when: - def personMeta = metadata(Person)?.entityMetadata() - def familyMeta = metadata(Family)?.entityMetadata() - - then: - personMeta?.attribute("name")?.get(johnDoe) == "John Doe" - familyMeta?.attribute("surname")?.get(doeFamily) == "Doe" - familyMeta?.attribute("members")?.get(doeFamily)?.size() == 2 - personMeta?.embeddedAttributes()?.size() == 1 - familyMeta?.linkedAttributes()?.size() == 1 - - } - -} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java new file mode 100644 index 000000000..b97adee34 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java @@ -0,0 +1,63 @@ +package org.springframework.data.rest.config; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonRepository; +import org.springframework.data.rest.repository.domain.jpa.PersonRepository; +import org.springframework.data.rest.repository.domain.jpa.PlainPersonRepository; + +/** + * Ensure the {@link ResourceMapping} components convey the correct information. + * + * @author Jon Brisbin + */ +public class ResourceMappingUnitTests { + + @Test + public void shouldDetectDefaultRelAndPath() throws Exception { + ResourceMapping mapping = new ResourceMapping( + findRel(PlainPersonRepository.class), + findPath(PlainPersonRepository.class), + findExported(PlainPersonRepository.class) + ); + + assertThat(mapping.getRel(), is("plainPerson")); + assertThat(mapping.getPath(), is("plainPerson")); + assertThat(mapping.isExported(), is(true)); + } + + @Test + public void shouldDetectAnnotatedRelAndPath() throws Exception { + ResourceMapping mapping = new ResourceMapping( + findRel(AnnotatedPersonRepository.class), + findPath(AnnotatedPersonRepository.class), + findExported(AnnotatedPersonRepository.class) + ); + + assertThat(mapping.getRel(), is("people")); + // The path is not set on the annotation so this should be the default from class name. + assertThat(mapping.getPath(), is("annotatedPerson")); + assertThat(mapping.isExported(), is(false)); + } + + @Test + public void shouldDetectAnnotatedRelAndPathOnMethod() throws Exception { + Method method = PersonRepository.class.getMethod("findByFirstName", String.class, Pageable.class); + ResourceMapping mapping = new ResourceMapping( + findRel(method), + findPath(method), + findExported(method) + ); + + assertThat(mapping.getRel(), is("firstname")); + assertThat(mapping.getPath(), is("firstname")); + assertThat(mapping.isExported(), is(true)); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java new file mode 100644 index 000000000..853bfaeaf --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java @@ -0,0 +1,37 @@ +package org.springframework.data.rest.repository; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.domain.jpa.ConfiguredPersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Tests to check that {@link ResourceMapping}s are handled correctly. + * + * @author Jon Brisbin + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = RepositoryTestsConfig.class) +public class RepositoryRestConfigurationIntegrationTests { + + @Autowired + RepositoryRestConfiguration config; + + @Test + public void shouldProvideResourceMappingForConfiguredRepository() throws Exception { + ResourceMapping mapping = config.getResourceMappingForRepository(ConfiguredPersonRepository.class); + + assertThat(mapping, notNullValue()); + assertThat(mapping.getRel(), is("people")); + assertThat(mapping.getPath(), is("people")); + assertThat(mapping.isExported(), is(false)); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java new file mode 100644 index 000000000..63cbf4cf0 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java @@ -0,0 +1,50 @@ +package org.springframework.data.rest.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.repository.domain.jpa.ConfiguredPersonRepository; +import org.springframework.data.rest.repository.domain.jpa.JpaRepositoryConfig; +import org.springframework.data.rest.repository.domain.jpa.Person; +import org.springframework.data.rest.repository.domain.jpa.PersonRepository; + +/** + * @author Jon Brisbin + */ +@Configuration +@Import({JpaRepositoryConfig.class}) +public class RepositoryTestsConfig { + + @Autowired + private ApplicationContext appCtx; + + @Bean public Repositories repositories() { + return new Repositories(appCtx); + } + + @Bean public RepositoryRestConfiguration config() { + RepositoryRestConfiguration config = new RepositoryRestConfiguration(); + + config.addResourceMappingForDomainType(Person.class) + .setRel("person"); + + config.setResourceMappingForRepository(ConfiguredPersonRepository.class) + .setRel("people") + .setPath("people") + .setExported(false); + + config.setResourceMappingForRepository(PersonRepository.class) + .setRel("people") + .setPath("people") + .addResourceMappingFor("findByFirstName") + .setRel("firstname") + .setPath("firstname"); + + return config; + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/ValidationErrors.properties b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/ValidationErrors.properties new file mode 100644 index 000000000..8e196e844 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/ValidationErrors.properties @@ -0,0 +1,2 @@ +field.name.required = Field {0}.{1} is required. +no.userid = {0}s must be assigned initial userids. diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventIntegrationTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventIntegrationTests.java new file mode 100644 index 000000000..f3506e815 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventIntegrationTests.java @@ -0,0 +1,73 @@ +package org.springframework.data.rest.repository.context; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.rest.repository.domain.jpa.Person; +import org.springframework.data.rest.repository.domain.jpa.PersonRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Tests around the {@link org.springframework.context.ApplicationEvent} handling abstractions. + * + * @author Jon Brisbin + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = RepositoryEventTestsConfig.class) +public class RepositoryEventIntegrationTests { + + @Autowired + ApplicationContext appCtx; + @Autowired + PersonRepository people; + Person person; + + @Before + public void setup() { + person = people.save(new Person("Jane", "Doe")); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchBeforeSave() throws Exception { + appCtx.publishEvent(new BeforeSaveEvent(person)); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchAfterSave() throws Exception { + appCtx.publishEvent(new AfterSaveEvent(person)); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchBeforeDelete() throws Exception { + appCtx.publishEvent(new BeforeDeleteEvent(person)); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchAfterDelete() throws Exception { + appCtx.publishEvent(new AfterDeleteEvent(person)); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchBeforeLinkSave() throws Exception { + appCtx.publishEvent(new BeforeLinkSaveEvent(person, new Object())); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchAfterLinkSave() throws Exception { + appCtx.publishEvent(new AfterLinkSaveEvent(person, new Object())); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchBeforeLinkDelete() throws Exception { + appCtx.publishEvent(new BeforeLinkDeleteEvent(person, new Object())); + } + + @Test(expected = RuntimeException.class) + public void shouldDispatchAfterLinkDelete() throws Exception { + appCtx.publishEvent(new AfterLinkDeleteEvent(person, new Object())); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventTestsConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventTestsConfig.java new file mode 100644 index 000000000..798d90777 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/RepositoryEventTestsConfig.java @@ -0,0 +1,29 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.rest.repository.RepositoryTestsConfig; +import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonEventHandler; +import org.springframework.data.rest.repository.domain.jpa.PersonBeforeSaveHandler; + +/** + * @author Jon Brisbin + */ +@Configuration +@Import({RepositoryTestsConfig.class}) +public class RepositoryEventTestsConfig { + + @Bean public PersonBeforeSaveHandler personBeforeSaveHandler() { + return new PersonBeforeSaveHandler(); + } + + @Bean public AnnotatedPersonEventHandler beforeSaveHandler() { + return new AnnotatedPersonEventHandler(); + } + + @Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() { + return new AnnotatedHandlerBeanPostProcessor(); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorIntegrationTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorIntegrationTests.java new file mode 100644 index 000000000..02241f192 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorIntegrationTests.java @@ -0,0 +1,29 @@ +package org.springframework.data.rest.repository.context; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.rest.repository.RepositoryConstraintViolationException; +import org.springframework.data.rest.repository.domain.jpa.Person; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Tests to check the {@link org.springframework.validation.Validator} integration. + * + * @author Jon Brisbin + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ValidatorTestsConfig.class) +public class ValidatorIntegrationTests { + + @Autowired + ApplicationContext appCtx; + + @Test(expected = RepositoryConstraintViolationException.class) + public void shouldValidateLastName() throws Exception { + appCtx.publishEvent(new BeforeSaveEvent(new Person())); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorTestsConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorTestsConfig.java new file mode 100644 index 000000000..b4cb7595d --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/context/ValidatorTestsConfig.java @@ -0,0 +1,19 @@ +package org.springframework.data.rest.repository.context; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.rest.repository.RepositoryTestsConfig; + +/** + * @author Jon Brisbin + */ +@Configuration +@Import({RepositoryTestsConfig.class}) +public class ValidatorTestsConfig { + + @Bean public ValidatingRepositoryEventListener validatingListener() { + return new ValidatingRepositoryEventListener(); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonEventHandler.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonEventHandler.java new file mode 100644 index 000000000..465750175 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonEventHandler.java @@ -0,0 +1,41 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.data.rest.repository.annotation.HandleAfterDelete; +import org.springframework.data.rest.repository.annotation.HandleAfterLinkDelete; +import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave; +import org.springframework.data.rest.repository.annotation.HandleAfterSave; +import org.springframework.data.rest.repository.annotation.HandleBeforeDelete; +import org.springframework.data.rest.repository.annotation.HandleBeforeLinkDelete; +import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave; +import org.springframework.data.rest.repository.annotation.HandleBeforeSave; +import org.springframework.data.rest.repository.annotation.RepositoryEventHandler; + +/** + * @author Jon Brisbin + */ +@RepositoryEventHandler(Person.class) +public class AnnotatedPersonEventHandler { + @HandleAfterDelete + @HandleAfterSave + public void handleAfter(Person p) { + throw new RuntimeException(); + } + + @HandleAfterLinkDelete + @HandleAfterLinkSave + public void handleAfterLink(Person p, Object o) { + throw new RuntimeException(); + } + + @HandleBeforeDelete + @HandleBeforeSave + public void handleBefore(Person p) { + throw new RuntimeException(); + } + + @HandleBeforeLinkDelete + @HandleBeforeLinkSave + public void handleBeforeLink(Person p, Object o) { + throw new RuntimeException(); + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonRepository.java new file mode 100644 index 000000000..c8746bdb3 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/AnnotatedPersonRepository.java @@ -0,0 +1,13 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.repository.annotation.RestResource; + +/** + * A repository to manage {@link org.springframework.data.rest.repository.domain.jpa.Person}s. + * + * @author Jon Brisbin + */ +@RestResource(rel = "people", exported = false) +public interface AnnotatedPersonRepository extends CrudRepository { +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java new file mode 100644 index 000000000..b95178388 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java @@ -0,0 +1,11 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.data.repository.CrudRepository; + +/** + * A repository to manage {@link Person}s. + * + * @author Jon Brisbin + */ +public interface ConfiguredPersonRepository extends CrudRepository { +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java similarity index 90% rename from spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationConfig.java rename to spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java index ca14fd52b..467f919b0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationConfig.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.test; +package org.springframework.data.rest.repository.domain.jpa; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; @@ -24,14 +24,14 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * @author Jon Brisbin */ @Configuration -@ComponentScan(basePackages = "org.springframework.data.rest.test.webmvc") +@ComponentScan(basePackageClasses = {JpaRepositoryConfig.class}) @EnableJpaRepositories @EnableTransactionManagement -public class ApplicationConfig { +public class JpaRepositoryConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); - ms.setBasename("org.springframework.data.rest.test.ValidationErrors"); + ms.setBasename("org.springframework.data.rest.repository.ValidationErrors"); return ms; } @@ -64,5 +64,4 @@ public class ApplicationConfig { txManager.setEntityManagerFactory(entityManagerFactory()); return txManager; } - } diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/Person.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/Person.java new file mode 100644 index 000000000..84c85d16f --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/Person.java @@ -0,0 +1,82 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.PrePersist; + +/** + * An entity that represents a person. + * + * @author Jon Brisbin + */ +@Entity +public class Person { + + @Id @GeneratedValue private Long id; + private String firstName; + private String lastName; + @OneToMany + private List siblings = Collections.emptyList(); + private Date created; + + public Person() { + } + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Person addSibling(Person p) { + if(siblings == Collections.EMPTY_LIST) { + siblings = new ArrayList(); + } + siblings.add(p); + return this; + } + + public List getSiblings() { + return siblings; + } + + public void setSiblings(List siblings) { + this.siblings = siblings; + } + + public Date getCreated() { + return created; + } + + @PrePersist + private void prePersist() { + this.created = Calendar.getInstance().getTime(); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonBeforeSaveHandler.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonBeforeSaveHandler.java new file mode 100644 index 000000000..529423ab6 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonBeforeSaveHandler.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.data.rest.repository.context.AbstractRepositoryEventListener; + +/** + * @author Jon Brisbin + */ +public class PersonBeforeSaveHandler extends AbstractRepositoryEventListener { + @Override protected void onBeforeSave(Person person) { + throw new RuntimeException(); + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java new file mode 100644 index 000000000..5f85fb70e --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java @@ -0,0 +1,20 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Jon Brisbin + */ +@Component +public class PersonLoader implements InitializingBean { + + @Autowired + private PlainPersonRepository people; + + @Override public void afterPropertiesSet() throws Exception { + people.save(new Person("John", "Doe")); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonNameValidator.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonNameValidator.java new file mode 100644 index 000000000..fa3deeb58 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonNameValidator.java @@ -0,0 +1,31 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import static org.springframework.util.ClassUtils.*; +import static org.springframework.util.StringUtils.*; + +import org.springframework.data.rest.repository.annotation.HandleBeforeSave; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * A test {@link Validator} that checks for non-blank names. + * + * @author Jon Brisbin + */ +@Component +@HandleBeforeSave +public class PersonNameValidator implements Validator { + + @Override public boolean supports(Class clazz) { + return isAssignable(clazz, Person.class); + } + + @Override public void validate(Object target, Errors errors) { + Person p = (Person)target; + if(!hasText(p.getLastName())) { + errors.rejectValue("lastName", "blank", "Last name cannot be blank"); + } + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonRepository.java new file mode 100644 index 000000000..7733c07b1 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonRepository.java @@ -0,0 +1,34 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import java.util.Date; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.convert.ISO8601DateConverter; +import org.springframework.data.rest.repository.annotation.ConvertWith; +import org.springframework.data.rest.repository.annotation.RestResource; + +/** + * A repository to manage {@link Person}s. + * + * @author Jon Brisbin + */ +@RestResource(rel = "people", path = "people") +public interface PersonRepository extends PagingAndSortingRepository { + + @RestResource(rel = "firstname", path = "firstname") + public Page findByFirstName(@Param("firstName") String firstName, Pageable pageable); + + public Page findByCreatedGreaterThan(@Param("date") Date date, Pageable pageable); + + @Query("select p from Person p where p.created > :date") + public Page findByCreatedUsingISO8601Date(@Param("date") + @ConvertWith( + ISO8601DateConverter.class) + Date date, + Pageable pageable); + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java new file mode 100644 index 000000000..624808b69 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java @@ -0,0 +1,11 @@ +package org.springframework.data.rest.repository.domain.jpa; + +import org.springframework.data.repository.CrudRepository; + +/** + * A repository to manage {@link Person}s. + * + * @author Jon Brisbin + */ +public interface PlainPersonRepository extends CrudRepository { +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/MongoDbRepositoryConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/MongoDbRepositoryConfig.java new file mode 100644 index 000000000..de49b7fdd --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/MongoDbRepositoryConfig.java @@ -0,0 +1,30 @@ +package org.springframework.data.rest.repository.domain.mongodb; + +import java.net.UnknownHostException; + +import com.mongodb.Mongo; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +/** + * @author Jon Brisbin + */ +@Configuration +@ComponentScan(basePackageClasses = {MongoDbRepositoryConfig.class}) +@EnableMongoRepositories +public class MongoDbRepositoryConfig { + + @Bean public MongoDbFactory mongoDbFactory() throws UnknownHostException { + return new SimpleMongoDbFactory(new Mongo("localhost"), "spring-data-rest"); + } + + @Bean public MongoTemplate mongoTemplate() throws UnknownHostException { + return new MongoTemplate(mongoDbFactory()); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/Profile.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/Profile.java new file mode 100644 index 000000000..f07561666 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/Profile.java @@ -0,0 +1,47 @@ +package org.springframework.data.rest.repository.domain.mongodb; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * @author Jon Brisbin + */ +@Document +public class Profile { + + @Id private String id; + private String name; + private String type; + + public Profile() { + } + + public Profile(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public Profile setName(String name) { + this.name = name; + return this; + } + + public String getType() { + return type; + } + + public Profile setType(String type) { + this.type = type; + return this; + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileLoader.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileLoader.java new file mode 100644 index 000000000..c1edbeb54 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileLoader.java @@ -0,0 +1,20 @@ +package org.springframework.data.rest.repository.domain.mongodb; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Jon Brisbin + */ +@Component +public class ProfileLoader implements InitializingBean { + + @Autowired + private ProfileRepository profiles; + + @Override public void afterPropertiesSet() throws Exception { + profiles.save(new Profile("jdoe", "jdoe", "account")); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileRepository.java new file mode 100644 index 000000000..737bbf8e2 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/mongodb/ProfileRepository.java @@ -0,0 +1,12 @@ +package org.springframework.data.rest.repository.domain.mongodb; + +import org.bson.types.ObjectId; +import org.springframework.data.repository.CrudRepository; + +/** + * Repository for managing {@link Profile}s in MongoDB. + * + * @author Jon Brisbin + */ +public interface ProfileRepository extends CrudRepository { +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionServiceUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionServiceUnitTests.java new file mode 100644 index 000000000..88cf92e6a --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionServiceUnitTests.java @@ -0,0 +1,70 @@ +package org.springframework.data.rest.repository.invoke; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.convert.ISO8601DateConverter; +import org.springframework.data.rest.repository.domain.jpa.PersonRepository; +import org.springframework.format.support.DefaultFormattingConversionService; + +/** + * @author Jon Brisbin + */ +public class MethodParameterConversionServiceUnitTests { + + static final SimpleDateFormat ISO8601_FMT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + static final String[] DATE_S = new String[]{"2010-01-01T12:00:00-0600"}; + static final Date DATE_D; + + static { + try { + DATE_D = ISO8601_FMT.parse(DATE_S[0]); + } catch(ParseException e) { + throw new IllegalStateException(e); + } + } + + MethodParameter findByCreatedGreaterThan; + MethodParameter findByCreatedUsingISO8601Date; + + @Before + public void setup() throws NoSuchMethodException { + findByCreatedGreaterThan = new MethodParameter(PersonRepository.class.getMethod("findByCreatedGreaterThan", + Date.class, + Pageable.class), 0); + findByCreatedUsingISO8601Date = new MethodParameter(PersonRepository.class.getMethod("findByCreatedUsingISO8601Date", + Date.class, + Pageable.class), 0); + } + + @SuppressWarnings({"deprecation"}) + @Test + public void shouldConvertDateParameterUsingDefaultConverter() throws Exception { + ConfigurableConversionService cs = new DefaultFormattingConversionService(); + MethodParameterConversionService conversionService = new MethodParameterConversionService(cs); + + String dateStr = "01/01/2010"; + assertThat(conversionService.canConvert(String.class, findByCreatedGreaterThan), is(true)); + assertThat((Date)conversionService.convert(dateStr, findByCreatedGreaterThan), is(new Date(dateStr))); + } + + @Test + public void shouldConvertDateParameterUsingSpecificConverter() throws Exception { + ConfigurableConversionService cs = new DefaultFormattingConversionService(); + cs.addConverter(ISO8601DateConverter.INSTANCE); + MethodParameterConversionService conversionService = new MethodParameterConversionService(cs); + + assertThat(conversionService.canConvert(String.class, findByCreatedUsingISO8601Date), is(true)); + assertThat((Date)conversionService.convert(DATE_S, findByCreatedUsingISO8601Date), is(DATE_D)); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/RepositoryMethodUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/RepositoryMethodUnitTests.java new file mode 100644 index 000000000..9db7c9173 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/RepositoryMethodUnitTests.java @@ -0,0 +1,73 @@ +package org.springframework.data.rest.repository.invoke; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.springframework.util.ReflectionUtils.*; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.repository.domain.jpa.PersonRepository; +import org.springframework.data.rest.repository.support.Methods; +import org.springframework.util.ReflectionUtils; + +/** + * Tests to verify the integrity of the {@link RepositoryMethod} abstraction. + * + * @author Jon Brisbin + */ +public class RepositoryMethodUnitTests { + + Map methods = new HashMap(); + RepositoryMethod method; + + @Before + public void setup() { + doWithMethods(PersonRepository.class, + new ReflectionUtils.MethodCallback() { + @Override public void doWith(Method method) throws IllegalArgumentException, + IllegalAccessException { + String name = method.getName(); + RepositoryMethod repoMethod = new RepositoryMethod(method); + methods.put(name, repoMethod); + } + }, + Methods.USER_METHODS); + method = methods.get("findByFirstName"); + } + + @Test + public void shouldFindSimpleQueryMethods() throws Exception { + assertThat(method, notNullValue()); + } + + @Test + public void shouldFindPageableInformationOnMethod() throws Exception { + assertThat(method, notNullValue()); + assertThat(method.isPageable(), is(true)); + } + + @Test + public void shouldNotFindSortInformationOnMethod() throws Exception { + assertThat(method, notNullValue()); + assertThat(method.isSortable(), is(false)); + } + + @Test + public void shouldProvideParameterClassTypes() throws Exception { + assertThat(method, notNullValue()); + assertThat(method.getParameters().get(0).getParameterType(), is(typeCompatibleWith(String.class))); + assertThat(method.getParameters().get(1).getParameterType(), is(typeCompatibleWith(Pageable.class))); + } + + @Test + public void shouldProvideParameterNames() throws Exception { + assertThat(method, notNullValue()); + assertThat(method.getParameterNames(), contains("firstName", "arg1")); + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Family.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Family.java deleted file mode 100644 index ecfba9869..000000000 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Family.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.springframework.data.rest.repository.test; - -import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToMany; - -/** - * @author Jon Brisbin - */ -@Entity -public class Family { - - @Id - @GeneratedValue - private Long id; - private String surname; - @OneToMany - private List members; - - public Family() { - } - - public Family(String surname) { - this.surname = surname; - } - - public Long getId() { - return id; - } - - public String getSurname() { - return surname; - } - - public void setSurname(String surname) { - this.surname = surname; - } - - public List getMembers() { - return members; - } - - public void setMembers(List members) { - this.members = members; - } - - @Override public String toString() { - return "Family{" + - "id=" + id + - ", surname='" + surname + '\'' + - ", members=" + members + - '}'; - } - -} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/FamilyRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/FamilyRepository.java deleted file mode 100644 index 5bdd778ea..000000000 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/FamilyRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.springframework.data.rest.repository.test; - -import org.springframework.data.repository.CrudRepository; - -/** - * @author Jon Brisbin - */ -public interface FamilyRepository extends CrudRepository { -} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Person.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Person.java deleted file mode 100644 index 2b342fbb7..000000000 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/Person.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.springframework.data.rest.repository.test; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -/** - * @author Jon Brisbin - */ -@Entity -public class Person { - - @Id - @GeneratedValue - private Long id; - private String name; - - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override public String toString() { - return "Person{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; - } - -} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/PersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/PersonRepository.java deleted file mode 100644 index 1d93d5812..000000000 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/PersonRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.springframework.data.rest.repository.test; - -import java.util.List; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * @author Jon Brisbin - */ -public interface PersonRepository extends CrudRepository { - - @RestResource(path = "byName") - public List findByName(String name); - -} diff --git a/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml b/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml deleted file mode 100644 index 0bfa9eb43..000000000 --- a/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/spring-data-rest-repository/src/test/resources/JpaMetadataSpec-test.xml b/spring-data-rest-repository/src/test/resources/JpaMetadataSpec-test.xml deleted file mode 100644 index 3fd6c23d4..000000000 --- a/spring-data-rest-repository/src/test/resources/JpaMetadataSpec-test.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-data-rest-webmvc/build.gradle b/spring-data-rest-webmvc/build.gradle deleted file mode 100644 index 708db35cf..000000000 --- a/spring-data-rest-webmvc/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -dependencies { - - // APIS - providedCompile "javax.servlet:javax.servlet-api:3.0.1" - - // JPA - compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" - - // JSR 303 Validation - compile "javax.validation:validation-api:1.0.0.GA" - runtime "org.hibernate:hibernate-validator-annotation-processor:4.1.0.Final" - - // Spring - compile "org.springframework:spring-webmvc:$springVersion" - - // Repository Exporter support - compile project(":spring-data-rest-repository") - - compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion" - - runtime "org.hibernate:hibernate-entitymanager:$hibernateVersion" - runtime "org.hsqldb:hsqldb:2.2.8" - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java new file mode 100644 index 000000000..3bf7e7224 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -0,0 +1,285 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.data.rest.core.util.UriUtils.*; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.validation.ConstraintViolationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.RepositoryConstraintViolationException; +import org.springframework.data.rest.repository.invoke.MethodParameterConversionService; +import org.springframework.data.rest.repository.support.ResourceMappingUtils; +import org.springframework.data.rest.webmvc.support.BaseUriLinkBuilder; +import org.springframework.data.rest.webmvc.support.ConstraintViolationExceptionMessage; +import org.springframework.data.rest.webmvc.support.ExceptionMessage; +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.data.rest.webmvc.support.RepositoryConstraintViolationExceptionMessage; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Jon Brisbin + */ +public class AbstractRepositoryRestController implements ApplicationContextAware { + + static final Resource EMPTY_RESOURCE = new Resource(Collections.emptyList()); + static final Resources> EMPTY_RESOURCES = new Resources>(Collections.>emptyList()); + static final Iterable> EMPTY_RESOURCE_LIST = Collections.emptyList(); + static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); + protected final Logger LOG = LoggerFactory.getLogger(getClass()); + protected final Repositories repositories; + protected final RepositoryRestConfiguration config; + protected final DomainClassConverter domainClassConverter; + protected final ConversionService conversionService; + protected final MethodParameterConversionService methodParameterConversionService; + protected ApplicationContext applicationContext; + + @Autowired + public AbstractRepositoryRestController(Repositories repositories, + RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, + ConversionService conversionService) { + this.repositories = repositories; + this.config = config; + this.domainClassConverter = domainClassConverter; + this.conversionService = conversionService; + this.methodParameterConversionService = new MethodParameterConversionService(conversionService); + } + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @ExceptionHandler({ + NullPointerException.class + }) + @ResponseBody + public ResponseEntity handleNPE(NullPointerException npe) { + return errorResponse(npe, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler({ + ResourceNotFoundException.class + }) + @ResponseBody + public ResponseEntity handleNotFound() { + return notFound(); + } + + @ExceptionHandler({ + NoSuchMethodError.class, + HttpRequestMethodNotSupportedException.class + }) + @ResponseBody + public ResponseEntity handleNoSuchMethod() { + return errorResponse(null, HttpStatus.METHOD_NOT_ALLOWED); + } + + @ExceptionHandler({ + HttpMessageNotReadableException.class, + HttpMessageNotWritableException.class + }) + @ResponseBody + public ResponseEntity handleNotReadable(HttpMessageNotReadableException e) { + return badRequest(e); + } + + /** + * Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type. + * + * @param t + * + * @return + * + * @throws java.io.IOException + */ + @ExceptionHandler({ + InvocationTargetException.class, + IllegalArgumentException.class, + ClassCastException.class, + ConversionFailedException.class + }) + @ResponseBody + public ResponseEntity handleMiscFailures(Throwable t) { + return badRequest(t); + } + + @ExceptionHandler({ + ConstraintViolationException.class + }) + @ResponseBody + public ResponseEntity handleConstraintViolationException(ConstraintViolationException cve) { + return response(null, + new ConstraintViolationExceptionMessage(cve, applicationContext), + HttpStatus.CONFLICT); + } + + @ExceptionHandler({ + RepositoryConstraintViolationException.class + }) + @ResponseBody + public ResponseEntity handleRepositoryConstraintViolationException(RepositoryConstraintViolationException rcve) { + return response(null, + new RepositoryConstraintViolationExceptionMessage(rcve, applicationContext), + HttpStatus.CONFLICT); + } + + /** + * Send a 409 Conflict in case of concurrent modification. + * + * @param ex + * + * @return + */ + @SuppressWarnings({"unchecked"}) + @ExceptionHandler({ + OptimisticLockingFailureException.class, + DataIntegrityViolationException.class + }) + @ResponseBody + public ResponseEntity handleConflict(Exception ex) { + return errorResponse(null, ex, HttpStatus.CONFLICT); + } + + protected ResponseEntity notFound() { + return notFound(null, null); + } + + protected ResponseEntity notFound(HttpHeaders headers, T body) { + return response(headers, body, HttpStatus.NOT_FOUND); + } + + protected ResponseEntity badRequest(T throwable) { + return badRequest(null, throwable); + } + + protected ResponseEntity badRequest(HttpHeaders headers, T throwable) { + return errorResponse(headers, throwable, HttpStatus.BAD_REQUEST); + } + + public ResponseEntity internalServerError(T throwable) { + return internalServerError(null, throwable); + } + + public ResponseEntity internalServerError(HttpHeaders headers, T throwable) { + return errorResponse(headers, throwable, HttpStatus.INTERNAL_SERVER_ERROR); + } + + public ResponseEntity errorResponse(T throwable, + HttpStatus status) { + return errorResponse(null, throwable, status); + } + + public ResponseEntity errorResponse(HttpHeaders headers, + T throwable, + HttpStatus status) { + LOG.error(throwable.getMessage(), throwable); + return response(headers, new ExceptionMessage(throwable), status); + } + + public ResponseEntity response(HttpHeaders headers, T body, HttpStatus status) { + HttpHeaders hdrs = new HttpHeaders(); + if(null != headers) { + hdrs.putAll(headers); + } + return new ResponseEntity(body, hdrs, status); + } + + public > ResponseEntity> resourceResponse(HttpHeaders headers, + R resource, + HttpStatus status) { + HttpHeaders hdrs = new HttpHeaders(); + if(null != headers) { + hdrs.putAll(headers); + } + return new ResponseEntity>(resource, hdrs, status); + } + + protected JsonpResponse jsonpWrapResponse(RepositoryRestRequest repoRequest, + T response, + HttpStatus status) { + return jsonpWrapResponse(repoRequest, response, null, status); + } + + protected JsonpResponse jsonpWrapResponse(RepositoryRestRequest repoRequest, + ResponseEntity response) { + return jsonpWrapResponse(repoRequest, + response.getBody(), + response.getHeaders(), + response.getStatusCode()); + } + + protected JsonpResponse jsonpWrapResponse(RepositoryRestRequest repoRequest, + T response, + HttpHeaders headers, + HttpStatus status) { + String callback = repoRequest.getRequest().getParameter(config.getJsonpParamName()); + String errback = repoRequest.getRequest().getParameter(config.getJsonpOnErrParamName()); + ResponseEntity newResponse; + if(null != headers) { + newResponse = new ResponseEntity(response, headers, status); + } else { + newResponse = new ResponseEntity(response, status); + } + return new JsonpResponse(newResponse, + (null != callback ? callback : config.getJsonpParamName()), + (null != errback ? errback : config.getJsonpOnErrParamName())); + } + + protected List queryMethodLinks(URI baseUri, Class domainType) { + List links = new ArrayList(); + RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType); + ResourceMapping repoMapping = ResourceMappingUtils.merge( + repoInfo.getRepositoryInterface(), + config.getResourceMappingForRepository(repoInfo.getRepositoryInterface()) + ); + for(Method method : repoInfo.getQueryMethods()) { + LinkBuilder linkBuilder = BaseUriLinkBuilder.create(buildUri(baseUri, repoMapping.getPath(), "search")); + ResourceMapping methodMapping = ResourceMappingUtils.merge(method, + repoMapping.getResourceMappingFor(method.getName())); + links.add(linkBuilder.slash(methodMapping.getPath()) + .withRel(repoMapping.getRel() + "." + methodMapping.getRel())); + } + return links; + } + + protected Link resourceLink(RepositoryRestRequest repoRequest, Resource resource) { + ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); + ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping(); + + Link selfLink = resource.getLink("self"); + String rel = repoMapping.getRel() + "." + entityMapping.getRel(); + return new Link(selfLink.getHref(), rel); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java index 02d58a968..3e1af82c0 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java @@ -5,7 +5,8 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; -import org.springframework.data.rest.webmvc.json.JsonSchemaController; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.annotation.BaseURI; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -17,13 +18,12 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; */ public class BaseUriMethodArgumentResolver implements HandlerMethodArgumentResolver { - @Autowired(required = false) - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; + @Autowired + private RepositoryRestConfiguration config; @Override public boolean supportsParameter(MethodParameter parameter) { - return (RepositoryRestController.class.isAssignableFrom(parameter.getDeclaringClass()) - || JsonSchemaController.class.isAssignableFrom(parameter.getDeclaringClass())) - && parameter.getParameterType() == URI.class; + return (null != parameter.getParameterAnnotation(BaseURI.class) + && parameter.getParameterType() == URI.class); } @Override diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityResource.java deleted file mode 100644 index 43c520bef..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityResource.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.net.URI; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.codehaus.jackson.annotate.JsonAnyGetter; -import org.springframework.data.rest.repository.AttributeMetadata; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; - -/** - * @author Jon Brisbin - */ -public class EntityResource extends Resource> { - - public EntityResource(Map dto, Set links) { - super(dto, links); - } - - @SuppressWarnings({"unchecked"}) - public static EntityResource wrap(Object entity, RepositoryMetadata repoMeta, URI baseUri) { - - Set links = new HashSet(); - for(Object attrName : repoMeta.entityMetadata().linkedAttributes().keySet()) { - URI uri = buildUri(baseUri, attrName.toString()); - String rel = repoMeta.rel() + "." + entity.getClass().getSimpleName() + "." + attrName; - links.add(new Link(uri.toString(), rel)); - } - links.add(new Link(baseUri.toString(), "self")); - - Map entityDto = new HashMap(); - for(Map.Entry attrMeta : ((Map)repoMeta.entityMetadata() - .embeddedAttributes()).entrySet()) { - String name = attrMeta.getKey(); - Object val; - if(null != (val = attrMeta.getValue().get(entity))) { - entityDto.put(name, val); - } - } - - return new EntityResource(entityDto, links); - } - - @JsonAnyGetter - @Override public Map getContent() { - return super.getContent(); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityToResourceConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityToResourceConverter.java deleted file mode 100644 index b8400bc11..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/EntityToResourceConverter.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.io.Serializable; -import java.net.URI; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.rest.repository.AttributeMetadata; -import org.springframework.data.rest.repository.EntityMetadata; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.util.Assert; - -/** - * A {@link Converter} to turn domain entities into {@link Resource}s by segregating embedded entities (those entities - * not managed by a {@link org.springframework.data.repository.Repository}) from linked or related entities (which - * don't get inlined into an entity's representation but are replaced by links instead. - * - * @author Jon Brisbin - */ -public class EntityToResourceConverter implements Converter { - - private final RepositoryRestConfiguration config; - private final RepositoryMetadata repositoryMetadata; - private final EntityMetadata entityMetadata; - - public EntityToResourceConverter(RepositoryRestConfiguration config, - RepositoryMetadata repositoryMetadata) { - this.config = config; - Assert.notNull(repositoryMetadata, "RepositoryMetadata cannot be null!"); - this.repositoryMetadata = repositoryMetadata; - this.entityMetadata = repositoryMetadata.entityMetadata(); - } - - @SuppressWarnings({"unchecked"}) - @Override public Resource convert(Object source) { - if(null == repositoryMetadata || null == source) { - return new Resource(source); - } - - Serializable id = (Serializable)repositoryMetadata.entityMetadata().idAttribute().get(source); - URI selfUri = buildUri(config.getBaseUri(), repositoryMetadata.name(), String.format("%s", id)); - - Set links = new HashSet(); - for(Object attrName : entityMetadata.linkedAttributes().keySet()) { - URI uri = buildUri(selfUri, attrName.toString()); - String rel = repositoryMetadata.rel() + "." + source.getClass().getSimpleName() + "." + attrName; - links.add(new Link(uri.toString(), rel)); - } - links.add(new Link(selfUri.toString(), "self")); - - Map entityDto = new HashMap(); - for(Map.Entry attrMeta : ((Map)entityMetadata.embeddedAttributes()) - .entrySet()) { - String name = attrMeta.getKey(); - Object val; - if(null != (val = attrMeta.getValue().get(source))) { - entityDto.put(name, val); - } - } - - return new EntityResource(entityDto, links); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java deleted file mode 100644 index f7bf284d7..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.List; - -import org.springframework.http.MediaType; - -/** - * @author Jon Brisbin - */ -public abstract class MediaTypes { - - private MediaTypes() { - } - - public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); - - public static final List ACCEPT_ALL_TYPES = Collections.singletonList(MediaType.ALL); - public static final MediaType COMPACT_JSON = new MediaType("application", - "x-spring-data-compact+json", - ISO_8859_1); - public static final MediaType VERBOSE_JSON = new MediaType("application", - "x-spring-data-verbose+json", - ISO_8859_1); - public static final MediaType APPLICATION_JAVASCRIPT = new MediaType("application", - "javascript", - ISO_8859_1); - public static final MediaType URI_LIST = new MediaType("text", - "uri-list", - ISO_8859_1); - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java index b34ef4324..6db3ace83 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java @@ -9,6 +9,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.support.PagingAndSorting; import org.springframework.data.web.PageableDefaults; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -27,8 +29,8 @@ public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgu private static final int DEFAULT_PAGE = 1; // We're 1-based, not 0-based - @Autowired(required = false) - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; + @Autowired + private RepositoryRestConfiguration config; @Override public boolean supportsParameter(MethodParameter parameter) { return ClassUtils.isAssignable(parameter.getParameterType(), PagingAndSorting.class); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..e0d60e53b --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java @@ -0,0 +1,59 @@ +package org.springframework.data.rest.webmvc; + +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; +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 PersistentEntityResourceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Autowired + private RepositoryRestRequestHandlerMethodArgumentResolver repoRequestResolver; + private final List> messageConverters; + + public PersistentEntityResourceHandlerMethodArgumentResolver(List> messageConverters) { + this.messageConverters = messageConverters; + } + + @Override public boolean supportsParameter(MethodParameter parameter) { + return PersistentEntityResource.class.isAssignableFrom(parameter.getParameterType()); + } + + @SuppressWarnings({"unchecked"}) + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + RepositoryRestRequest repoRequest = (RepositoryRestRequest)repoRequestResolver.resolveArgument(parameter, + mavContainer, + webRequest, + binderFactory); + + final ServletServerHttpRequest request = new ServletServerHttpRequest(webRequest.getNativeRequest(HttpServletRequest.class)); + for(HttpMessageConverter converter : messageConverters) { + Class domainType = repoRequest.getPersistentEntity().getType(); + if(!converter.canRead(domainType, request.getHeaders().getContentType())) { + continue; + } + + Object obj = converter.read(domainType, request); + return new PersistentEntityResource(repoRequest.getPersistentEntity(), + obj); + } + + return null; + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java deleted file mode 100644 index 66a52f6c4..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.codehaus.jackson.JsonEncoding; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.map.Module; -import org.codehaus.jackson.map.ObjectMapper; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.rest.repository.RepositoryExporter; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.data.rest.repository.UriToDomainObjectUriResolver; -import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; - -/** - * @author Jon Brisbin - */ -public class RepositoryAwareMappingHttpMessageConverter - extends MappingJacksonHttpMessageConverter - implements ApplicationEventPublisherAware, - InitializingBean { - - private final ObjectMapper mapper = new ObjectMapper(); - @Autowired(required = false) - protected List conversionServices = Arrays.asList(new DefaultFormattingConversionService()); - @Autowired(required = false) - protected List repositoryExporters = Collections.emptyList(); - @Autowired(required = false) - protected List modules = Collections.emptyList(); - @Autowired - protected UriToDomainObjectUriResolver domainObjectResolver = null; - @Autowired - protected RepositoryAwareJacksonModule jacksonModule = null; - protected ApplicationEventPublisher eventPublisher = null; - - public RepositoryAwareMappingHttpMessageConverter() { - setSupportedMediaTypes(Arrays.asList( - MediaType.APPLICATION_JSON, - MediaTypes.COMPACT_JSON, - MediaTypes.VERBOSE_JSON - )); - setObjectMapper(mapper); - } - - @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.eventPublisher = applicationEventPublisher; - } - - @Override public void afterPropertiesSet() throws Exception { - boolean builtInModuleRegistered = false; - for(Module m : modules) { - mapper.registerModule(m); - if(m.getClass() == RepositoryAwareJacksonModule.class) { - builtInModuleRegistered = true; - } - } - - if(!builtInModuleRegistered) { - mapper.registerModule(jacksonModule); - } - } - - public List getConversionServices() { - return conversionServices; - } - - public RepositoryAwareMappingHttpMessageConverter setConversionServices(List conversionServices) { - this.conversionServices = conversionServices; - return this; - } - - public List getRepositoryExporters() { - return repositoryExporters; - } - - @SuppressWarnings({"unchecked"}) - public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List repositoryExporters) { - this.repositoryExporters = repositoryExporters; - return this; - } - - public List getModules() { - return modules; - } - - public RepositoryAwareMappingHttpMessageConverter setModules(List modules) { - this.modules = modules; - return this; - } - - public UriToDomainObjectUriResolver getDomainObjectResolver() { - return domainObjectResolver; - } - - public RepositoryAwareMappingHttpMessageConverter setDomainObjectResolver(UriToDomainObjectUriResolver domainObjectResolver) { - this.domainObjectResolver = domainObjectResolver; - return this; - } - - @Override public boolean canWrite(Class clazz, MediaType mediaType) { - if(!canWrite(mediaType)) { - return false; - } - return supports(clazz); - } - - @Override public boolean canRead(Class clazz, MediaType mediaType) { - if(!canRead(mediaType)) { - return false; - } - return supports(clazz); - } - - @SuppressWarnings({"unchecked"}) - @Override protected boolean supports(Class clazz) { - for(RepositoryExporter repoExp : repositoryExporters) { - for(String repoName : new ArrayList(repoExp.repositoryNames())) { - RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName); - Class domainType = repoMeta.entityMetadata().type(); - if(domainType.isAssignableFrom(clazz)) { - return true; - } - } - } - return false; - } - - @Override protected void writeInternal(Object object, - HttpOutputMessage outputMessage) throws IOException, - HttpMessageNotWritableException { - JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); - // Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration - JsonGenerator jsonGenerator = mapper - .getJsonFactory() - .createJsonGenerator(outputMessage.getBody(), encoding) - .useDefaultPrettyPrinter(); - try { - mapper.writeValue(jsonGenerator, object); - } catch(IOException ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - } - - @Override protected Object readInternal(Class clazz, - HttpInputMessage inputMessage) throws IOException, - HttpMessageNotReadableException { - try { - return mapper.readValue(inputMessage.getBody(), clazz); - } catch(IOException ex) { - throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); - } - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java new file mode 100644 index 000000000..94970c17b --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -0,0 +1,65 @@ +package org.springframework.data.rest.webmvc; + +import static java.util.Collections.*; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.repository.support.RepositoryEntityLinks; +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Jon Brisbin + */ +@Controller +@RequestMapping("/") +public class RepositoryController extends AbstractRepositoryRestController { + + public RepositoryController(Repositories repositories, + RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, + ConversionService conversionService) { + super(repositories, config, domainClassConverter, conversionService); + } + + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-compact+json" + } + ) + @ResponseBody + public Resource listRepositories(RepositoryRestRequest repoRequest) + throws ResourceNotFoundException { + EntityLinks linkBuilder = new RepositoryEntityLinks(repoRequest.getBaseUri(), + repositories, + config); + Resource links = new Resource(emptyList()); + for(Class domainType : repositories) { + links.add(linkBuilder.linkToCollectionResource(domainType)); + } + return links; + } + + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse> jsonpListRepositories(RepositoryRestRequest repoRequest) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, listRepositories(repoRequest), HttpStatus.OK); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java new file mode 100644 index 000000000..040b6a77e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -0,0 +1,374 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.data.rest.core.util.UriUtils.*; + +import java.io.Serializable; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.data.rest.repository.context.AfterDeleteEvent; +import org.springframework.data.rest.repository.context.AfterSaveEvent; +import org.springframework.data.rest.repository.context.BeforeDeleteEvent; +import org.springframework.data.rest.repository.context.BeforeSaveEvent; +import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.json.JsonSchema; +import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.rest.repository.support.DomainObjectMerger; +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Jon Brisbin + */ +@Controller +@RequestMapping("/{repository}") +public class RepositoryEntityController extends AbstractRepositoryRestController { + + @Autowired + private DomainObjectMerger domainObjectMerger; + @Autowired + private PersistentEntityToJsonSchemaConverter jsonSchemaConverter; + + public RepositoryEntityController(Repositories repositories, + RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, + ConversionService conversionService) { + super(repositories, config, domainClassConverter, conversionService); + } + + @RequestMapping( + value = "/schema", + method = RequestMethod.GET, + produces = { + "application/schema+json" + } + ) + @ResponseBody + public JsonSchema schema(RepositoryRestRequest repoRequest) { + return jsonSchemaConverter.convert(repoRequest.getPersistentEntity().getType()); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-verbose+json" + } + ) + @ResponseBody + public Resources> listEntities(RepositoryRestRequest repoRequest) + throws ResourceNotFoundException { + List> resources = new ArrayList>(); + List links = new ArrayList(); + + Iterable results; + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + boolean hasPagingParams = (null != repoRequest.getRequest().getParameter(config.getPageParamName())); + boolean hasSortParams = (null != repoRequest.getRequest().getParameter(config.getSortParamName())); + if(repoMethodInvoker.hasFindAllPageable() && hasPagingParams) { + results = repoMethodInvoker.findAll(new PageRequest(repoRequest.getPagingAndSorting().getPageNumber(), + repoRequest.getPagingAndSorting().getPageSize(), + repoRequest.getPagingAndSorting().getSort())); + } else if(repoMethodInvoker.hasFindAllSorted() && hasSortParams) { + results = repoMethodInvoker.findAll(repoRequest.getPagingAndSorting().getSort()); + } else if(repoMethodInvoker.hasFindAll()) { + results = repoMethodInvoker.findAll(); + } else { + throw new ResourceNotFoundException(); + } + + for(Object o : results) { + resources.add(new PersistentEntityResource(repoRequest.getPersistentEntity(), + o, + repoRequest.buildEntitySelfLink(o, conversionService)) + .setBaseUri(repoRequest.getBaseUri())); + } + + + if(!repoMethodInvoker.getQueryMethods().isEmpty()) { + ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); + links.add(new Link(buildUri(repoRequest.getBaseUri(), repoMapping.getPath(), "search").toString(), + repoMapping.getRel() + ".search")); + } + + return new Resources>(resources, links); + } + + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse>> jsonpListEntities(RepositoryRestRequest repoRequest) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, listEntities(repoRequest), HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/x-spring-data-compact+json", + "text/uri-list" + } + ) + @ResponseBody + public Resources> listEntitiesCompact(RepositoryRestRequest repoRequest) + throws ResourceNotFoundException { + Resources> resources = listEntities(repoRequest); + List links = new ArrayList(resources.getLinks()); + + for(Resource resource : resources.getContent()) { + PersistentEntityResource persistentEntityResource = (PersistentEntityResource)resource; + links.add(resourceLink(repoRequest, persistentEntityResource)); + } + + return new Resources>(EMPTY_RESOURCE_LIST, links); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.POST, + consumes = { + "application/json" + }, + produces = { + "application/json", + "text/uri-list" + } + ) + @ResponseBody + public ResponseEntity> createNewEntity(RepositoryRestRequest repoRequest, + PersistentEntityResource incoming) { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasSaveOne()) { + throw new NoSuchMethodError(); + } + + applicationContext.publishEvent(new BeforeSaveEvent(incoming.getContent())); + Object obj = repoMethodInvoker.save(incoming.getContent()); + applicationContext.publishEvent(new AfterSaveEvent(obj)); + + Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create(selfLink.getHref())); + + return resourceResponse(headers, + new PersistentEntityResource(repoRequest.getPersistentEntity(), + obj, + selfLink) + .setBaseUri(repoRequest.getBaseUri()), + HttpStatus.CREATED); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.POST, + consumes = { + "application/json" + }, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse> jsonpCreateNewEntity(RepositoryRestRequest repoRequest, + PersistentEntityResource incoming) { + return jsonpWrapResponse(repoRequest, createNewEntity(repoRequest, incoming)); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{id}", + method = RequestMethod.GET, + produces = { + "application/json" + } + ) + @ResponseBody + public Resource getSingleEntity(RepositoryRestRequest repoRequest, + @PathVariable String id) + throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasFindOne()) { + throw new ResourceNotFoundException(); + } + + Object domainObj = domainClassConverter.convert(id, + STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity() + .getType())); + if(null == domainObj) { + throw new ResourceNotFoundException(); + } + + PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(), + domainObj, + repoRequest.getBaseUri()); + per.add(repoRequest.buildEntitySelfLink(domainObj, conversionService)); + return per; + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{id}", + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse> jsonpGetSingleEntity(RepositoryRestRequest repoRequest, + @PathVariable String id) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, + getSingleEntity(repoRequest, id), + HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{id}", + method = RequestMethod.PUT, + consumes = { + "application/json" + }, + produces = { + "application/json", + "text/uri-list" + } + ) + @ResponseBody + public ResponseEntity> updateEntity(RepositoryRestRequest repoRequest, + PersistentEntityResource incoming, + @PathVariable String id) + throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasSaveOne() || !repoMethodInvoker.hasFindOne()) { + throw new NoSuchMethodError(); + } + + Object domainObj = domainClassConverter.convert(id, + STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity() + .getType())); + if(null == domainObj) { + BeanWrapper incomingWrapper = BeanWrapper.create(incoming.getContent(), conversionService); + PersistentProperty idProp = incoming.getPersistentEntity().getIdProperty(); + incomingWrapper.setProperty(idProp, conversionService.convert(id, idProp.getType())); + return createNewEntity(repoRequest, incoming); + } + + domainObjectMerger.merge(incoming.getContent(), domainObj); + + applicationContext.publishEvent(new BeforeSaveEvent(incoming.getContent())); + Object obj = repoMethodInvoker.save(domainObj); + applicationContext.publishEvent(new AfterSaveEvent(obj)); + + PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(), + obj, + repoRequest.getBaseUri()); + per.add(repoRequest.buildEntitySelfLink(obj, conversionService)); + return resourceResponse(null, + per, + HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{id}", + method = RequestMethod.PUT, + consumes = { + "application/json" + }, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse> jsonpUpdateEntity(RepositoryRestRequest repoRequest, + PersistentEntityResource incoming, + @PathVariable String id) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, updateEntity(repoRequest, incoming, id)); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{id}", + method = RequestMethod.DELETE + ) + @ResponseBody + public ResponseEntity deleteEntity(RepositoryRestRequest repoRequest, + @PathVariable String id) + throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasFindOne() && + !(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker.hasDeleteOneById())) { + throw new NoSuchMethodError(); + } + + Object domainObj = domainClassConverter.convert(id, + STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity() + .getType())); + if(null == domainObj) { + throw new ResourceNotFoundException(); + } + + applicationContext.publishEvent(new BeforeDeleteEvent(domainObj)); + if(repoMethodInvoker.hasDeleteOneById()) { + Class idType = (Class)repoRequest.getPersistentEntity() + .getIdProperty() + .getType(); + Object idVal = conversionService.convert(id, idType); + repoMethodInvoker.delete((Serializable)idVal); + } else if(repoMethodInvoker.hasDeleteOne()) { + repoMethodInvoker.delete(domainObj); + } + applicationContext.publishEvent(new AfterDeleteEvent(domainObj)); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "{id}", + method = RequestMethod.DELETE, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpDeleteEntity(RepositoryRestRequest repoRequest, + @PathVariable String id) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, deleteEntity(repoRequest, id)); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityLinksMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityLinksMethodArgumentResolver.java new file mode 100644 index 000000000..8bf58e318 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityLinksMethodArgumentResolver.java @@ -0,0 +1,42 @@ +package org.springframework.data.rest.webmvc; + +import java.net.URI; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.rest.repository.support.RepositoryEntityLinks; +import org.springframework.data.rest.repository.support.RepositoryInformationSupport; +import org.springframework.hateoas.EntityLinks; +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 RepositoryEntityLinksMethodArgumentResolver + extends RepositoryInformationSupport + implements HandlerMethodArgumentResolver { + + @Autowired + private BaseUriMethodArgumentResolver baseUriResolver; + + @Override public boolean supportsParameter(MethodParameter parameter) { + return EntityLinks.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws Exception { + URI baseUri = (URI)baseUriResolver.resolveArgument(parameter, + mavContainer, + webRequest, + binderFactory); + return new RepositoryEntityLinks(baseUri, repositories, config); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..75d43c763 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java @@ -0,0 +1,48 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.util.ClassUtils.*; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.rest.repository.support.RepositoryInformationSupport; +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; +import org.springframework.web.util.UrlPathHelper; + +/** + * @author Jon Brisbin + */ +public class RepositoryInformationHandlerMethodArgumentResolver + extends RepositoryInformationSupport + implements HandlerMethodArgumentResolver { + + @Override public boolean supportsParameter(MethodParameter parameter) { + return isAssignable(parameter.getParameterType(), RepositoryInformation.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + String requestUri = new UrlPathHelper().getLookupPathForRequest(request); + if(requestUri.startsWith("/")) { + requestUri = requestUri.substring(1); + } + + String[] parts = requestUri.split("/"); + if(parts.length == 0) { + // Root request + return null; + } + + return findRepositoryInfoFor(parts[0]); + } + + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java new file mode 100644 index 000000000..1268bcc0d --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -0,0 +1,518 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.data.rest.core.util.UriUtils.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Function; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent; +import org.springframework.data.rest.repository.context.AfterLinkSaveEvent; +import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent; +import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent; +import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Jon Brisbin + */ +@Controller +@RequestMapping("/{repository}/{id}/{property}") +public class RepositoryPropertyReferenceController extends AbstractRepositoryRestController { + + public RepositoryPropertyReferenceController(Repositories repositories, + RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, + ConversionService conversionService) { + super(repositories, config, domainClassConverter, conversionService); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-verbose+json" + } + ) + @ResponseBody + public ResponseEntity> followPropertyReference(final RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property) + throws ResourceNotFoundException, NoSuchMethodException { + final HttpHeaders headers = new HttpHeaders(); + Function> handler = new Function>() { + @Override public Resource apply(ReferencedProperty prop) { + if(prop.property.isCollectionLike()) { + List> resources = new ArrayList>(); + PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); + for(Object obj : ((Iterable)prop.propertyValue)) { + PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri()); + Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService); + per.add(selfLink); + resources.add(per); + } + + return new Resource(resources); + } else if(prop.property.isMap()) { + Map> resources = new HashMap>(); + PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); + for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { + PersistentEntityResource per = PersistentEntityResource.wrap(entity, + entry.getValue(), + repoRequest.getBaseUri()); + Link selfLink = repoRequest.buildEntitySelfLink(entry.getValue(), conversionService); + per.add(selfLink); + resources.put(entry.getKey(), per); + } + + return new Resource(resources); + } else { + PersistentEntityResource per = PersistentEntityResource.wrap(repositories.getPersistentEntity(prop.propertyType), + prop.propertyValue, + repoRequest.getBaseUri()); + Link selfLink = repoRequest.buildEntitySelfLink(prop.propertyValue, conversionService); + per.add(selfLink); + + headers.set("Content-Location", selfLink.getHref()); + + return new Resource(per); + } + } + }; + Resource responseResource = doWithReferencedProperty(repoRequest, + id, + property, + handler); + return resourceResponse(headers, responseResource, HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{propertyId}", + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-verbose+json" + } + ) + @ResponseBody + public ResponseEntity> followPropertyReference(final RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property, + final @PathVariable String propertyId) + throws ResourceNotFoundException, NoSuchMethodException { + final HttpHeaders headers = new HttpHeaders(); + Function> handler = new Function>() { + @Override public Resource apply(ReferencedProperty prop) { + if(prop.property.isCollectionLike()) { + PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); + for(Object obj : ((Iterable)prop.propertyValue)) { + BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService); + String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); + if(propertyId.equals(sId)) { + PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri()); + Link selfLink = repoRequest.buildEntitySelfLink(obj, conversionService); + per.add(selfLink); + headers.set("Content-Location", selfLink.getHref()); + return new Resource(per); + } + } + } else if(prop.property.isMap()) { + PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); + for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { + BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService); + String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); + if(propertyId.equals(sId)) { + PersistentEntityResource per = PersistentEntityResource.wrap(entity, + entry.getValue(), + repoRequest.getBaseUri()); + Link selfLink = repoRequest.buildEntitySelfLink(entry.getValue(), conversionService); + per.add(selfLink); + headers.set("Content-Location", selfLink.getHref()); + return new Resource(per, selfLink); + } + } + } else { + return new Resource(prop.propertyValue); + } + throw new IllegalArgumentException(new ResourceNotFoundException()); + } + }; + Resource responseResource = doWithReferencedProperty(repoRequest, + id, + property, + handler); + return resourceResponse(headers, responseResource, HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/x-spring-data-compact+json", + "text/uri-list" + } + ) + @ResponseBody + public ResponseEntity> followPropertyReferenceCompact(RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property) + throws ResourceNotFoundException, NoSuchMethodException { + ResponseEntity> response = followPropertyReference(repoRequest, id, property); + if(response.getStatusCode() != HttpStatus.OK) { + return response; + } + + ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); + ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping(); + ResourceMapping propMapping = entityMapping.getResourceMappingFor(entityMapping.getNameForPath(property)); + String propRel = (null != propMapping ? propMapping.getRel() : property); + + Resource resource = response.getBody(); + + List links = new ArrayList(); + + URI entityBaseUri = buildUri(repoRequest.getBaseUri(), + repoMapping.getPath(), + id, + property); + + if(resource.getContent() instanceof Iterable) { + for(Resource res : (Iterable>)resource.getContent()) { + Link propLink = propertyReferenceLink(res, entityBaseUri, propRel); + links.add(propLink); + } + } else if(resource.getContent() instanceof Map) { + for(Map.Entry> entry : ((Map>)resource.getContent()).entrySet()) { + Link l = new Link(entry.getValue().getLink("self").getHref(), conversionService.convert(entry.getKey(), + String.class)); + links.add(l); + } + } else { + links.add(new Link(entityBaseUri.toString(), propRel)); + } + + return resourceResponse(null, new Resource(EMPTY_RESOURCE_LIST, links), HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpFollowPropertyReference(RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property) + throws ResourceNotFoundException, NoSuchMethodException { + return jsonpWrapResponse(repoRequest, + followPropertyReference(repoRequest, + id, + property), + HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{propertyId}", + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpFollowPropertyReference(RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property, + @PathVariable String propertyId) + throws ResourceNotFoundException, NoSuchMethodException { + return jsonpWrapResponse(repoRequest, + followPropertyReference(repoRequest, + id, + property, + propertyId), + HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = { + RequestMethod.POST, + RequestMethod.PUT + }, + consumes = { + "application/json", + "application/x-spring-data-compact+json", + "text/uri-list" + } + ) + @ResponseBody + public ResponseEntity> createPropertyReference(final RepositoryRestRequest repoRequest, + final @RequestBody Resource incoming, + @PathVariable String id, + @PathVariable String property) + throws ResourceNotFoundException, NoSuchMethodException { + final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasSaveOne()) { + throw new NoSuchMethodException(); + } + Function> handler = new Function>() { + @Override public Resource apply(ReferencedProperty prop) { + if(prop.property.isCollectionLike()) { + Collection coll = new ArrayList(); + if("POST".equals(repoRequest.getRequest().getMethod())) { + coll.addAll((Collection)prop.propertyValue); + } + for(Link l : incoming.getLinks()) { + Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); + coll.add(propVal); + } + prop.wrapper.setProperty(prop.property, coll); + } else if(prop.property.isMap()) { + Map m = new HashMap(); + if("POST".equals(repoRequest.getRequest().getMethod())) { + m.putAll((Map)prop.propertyValue); + } + for(Link l : incoming.getLinks()) { + Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); + m.put(l.getRel(), propVal); + } + prop.wrapper.setProperty(prop.property, m); + } else { + if("POST".equals(repoRequest.getRequest().getMethod())) { + throw new IllegalStateException( + "Cannot POST a reference to this singular property since the property type is not a List or a Map."); + } + if(incoming.getLinks().size() != 1) { + throw new IllegalArgumentException( + "Must send only 1 link to update a property reference that isn't a List or a Map."); + } + Object propVal = loadPropertyValue(prop.propertyType, incoming.getLinks().get(0).getHref()); + prop.wrapper.setProperty(prop.property, propVal); + } + + applicationContext.publishEvent(new BeforeLinkSaveEvent(prop.wrapper.getBean(), prop.propertyValue)); + Object result = repoMethodInvoker.save(prop.wrapper.getBean()); + applicationContext.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); + return null; + } + }; + doWithReferencedProperty(repoRequest, + id, + property, + handler); + return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + method = { + RequestMethod.POST, + RequestMethod.PUT + }, + consumes = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpCreatePropertyReference(final RepositoryRestRequest repoRequest, + final @RequestBody Resource incoming, + @PathVariable String id, + @PathVariable String property) + throws ResourceNotFoundException, NoSuchMethodException { + return jsonpWrapResponse(repoRequest, createPropertyReference(repoRequest, + incoming, + id, + property)); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{propertyId}", + method = RequestMethod.DELETE + ) + @ResponseBody + public ResponseEntity> deletePropertyReference(final RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property, + final @PathVariable String propertyId) + throws ResourceNotFoundException, NoSuchMethodException { + final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasDeleteOne()) { + throw new NoSuchMethodException(); + } + + Function> handler = new Function>() { + @Override public Resource apply(ReferencedProperty prop) { + if(null == prop.propertyValue) { + return null; + } + if(prop.property.isCollectionLike()) { + Collection coll = new ArrayList(); + for(Object obj : (Collection)prop.propertyValue) { + BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService); + String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false); + if(!propertyId.equals(s)) { + coll.add(obj); + } + } + prop.wrapper.setProperty(prop.property, coll); + } else if(prop.property.isMap()) { + Map m = new HashMap(); + for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { + BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService); + String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false); + if(!propertyId.equals(s)) { + m.put(entry.getKey(), entry.getValue()); + } + } + prop.wrapper.setProperty(prop.property, m); + } else { + prop.wrapper.setProperty(prop.property, null); + } + + applicationContext.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); + Object result = repoMethodInvoker.save(prop.wrapper.getBean()); + applicationContext.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); + return null; + } + }; + doWithReferencedProperty(repoRequest, + id, + property, + handler); + + return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{propertyId}", + method = RequestMethod.DELETE, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpDeletePropertyReference(final RepositoryRestRequest repoRequest, + @PathVariable String id, + @PathVariable String property, + final @PathVariable String propertyId) + throws ResourceNotFoundException, NoSuchMethodException { + return jsonpWrapResponse(repoRequest, deletePropertyReference(repoRequest, + id, + property, + propertyId)); + } + + private Link propertyReferenceLink(Resource resource, + URI baseUri, + String rel) { + Link selfLink = resource.getLink("self"); + String objId = selfLink.getHref().substring(selfLink.getHref().lastIndexOf('/') + 1); + return new Link(buildUri(baseUri, objId).toString(), rel); + } + + private Object loadPropertyValue(Class type, String href) { + String id = href.substring(href.lastIndexOf('/') + 1); + return domainClassConverter.convert(id, + STRING_TYPE, + TypeDescriptor.valueOf(type)); + } + + @SuppressWarnings({"unchecked"}) + private Resource doWithReferencedProperty(RepositoryRestRequest repoRequest, + String id, + String propertyPath, + Function> handler) + throws ResourceNotFoundException, NoSuchMethodException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(!repoMethodInvoker.hasFindOne()) { + throw new NoSuchMethodException(); + } + + Object domainObj = domainClassConverter.convert(id, + STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity() + .getType())); + if(null == domainObj) { + throw new ResourceNotFoundException(); + } + + String propertyName = repoRequest.getPersistentEntityResourceMapping().getNameForPath(propertyPath); + PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyName); + if(null == prop) { + throw new ResourceNotFoundException(); + } + + BeanWrapper wrapper = BeanWrapper.create(domainObj, conversionService); + Object propVal = wrapper.getProperty(prop); + if(null == propVal) { + throw new ResourceNotFoundException(); + } + + return handler.apply(new ReferencedProperty(prop, + propVal, + wrapper)); + } + + private class ReferencedProperty { + final PersistentEntity entity; + final PersistentProperty property; + final Class propertyType; + final Object propertyValue; + final BeanWrapper wrapper; + final RepositoryInformation propertyRepoInfo; + final Object propertyRepo; + final RepositoryMethodInvoker repoMethodInvoker; + + private ReferencedProperty(PersistentProperty property, + Object propertyValue, + BeanWrapper wrapper) { + this.property = property; + this.propertyValue = propertyValue; + this.wrapper = wrapper; + if(property.isCollectionLike()) { + this.propertyType = property.getComponentType(); + } else if(property.isMap()) { + this.propertyType = property.getMapValueType(); + } else { + this.propertyType = property.getType(); + } + this.propertyRepoInfo = repositories.getRepositoryInformationFor(propertyType); + this.entity = repositories.getPersistentEntity(propertyType); + this.propertyRepo = repositories.getRepositoryFor(entity.getType()); + this.repoMethodInvoker = new RepositoryMethodInvoker(propertyRepo, propertyRepoInfo, entity); + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java deleted file mode 100644 index ea96e4782..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ /dev/null @@ -1,1923 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.concurrent.atomic.AtomicReference; -import javax.servlet.http.HttpServletRequest; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; - -import org.codehaus.jackson.map.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionFailedException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.repository.Repository; -import org.springframework.data.rest.core.Handler; -import org.springframework.data.rest.core.convert.DelegatingConversionService; -import org.springframework.data.rest.repository.AttributeMetadata; -import org.springframework.data.rest.repository.RepositoryConstraintViolationException; -import org.springframework.data.rest.repository.RepositoryExporter; -import org.springframework.data.rest.repository.RepositoryExporterSupport; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.data.rest.repository.RepositoryNotFoundException; -import org.springframework.data.rest.repository.UriToDomainObjectUriResolver; -import org.springframework.data.rest.repository.annotation.RestResource; -import org.springframework.data.rest.repository.context.AfterDeleteEvent; -import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent; -import org.springframework.data.rest.repository.context.AfterLinkSaveEvent; -import org.springframework.data.rest.repository.context.AfterSaveEvent; -import org.springframework.data.rest.repository.context.BeforeDeleteEvent; -import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent; -import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent; -import org.springframework.data.rest.repository.context.BeforeSaveEvent; -import org.springframework.data.rest.repository.context.RepositoryEvent; -import org.springframework.data.rest.repository.invoke.CrudMethod; -import org.springframework.data.rest.repository.invoke.MethodParameterConversionService; -import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.PagedResources; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.Resources; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * Exports Spring Data Repositories over the web in a RESTful - * manner that is HATEOAS friendly. - *

- * This controller can be deployed in it's own DispatcherServlet. In that case, use the {@link - * RepositoryRestExporterServlet} in your web.xml. For example, to send all requests through the REST exporter, add the - * following to your web.xml: - *

- *

<servlet>
- *   <servlet-name>exporter</servlet-name>
- *   <servlet-class>org.springframework.data.rest.webmvc.RepositoryRestExporterServlet</servlet-class>
- *   <load-on-startup>1</load-on-startup>
- * </servlet>
- * 

- * <servlet-mapping> - * <servlet-name>exporter</servlet-name> - * <url-pattern>/*</url-pattern> - * </servlet-mapping> - *

- *

- * One can also deploy this controller into an existing Spring MVC application. In general, one should be able to - * simply create an instance of the {@link RepositoryRestMvcConfiguration} bean in your ApplicationContext - * or in JavaConfig. - *

- * If you wish to alter the way the REST exporter functions, you don't configure the controller directly. Instead there - * is a {@link RepositoryRestConfiguration} helper class that you create in your ApplicationContext. If a feature is - * configurable in Spring Data REST, there is a property on this helper to configure it. - * - * @author Jon Brisbin - */ -public class RepositoryRestController - extends RepositoryExporterSupport - implements ApplicationContextAware, - InitializingBean { - - public static final String LOCATION = "Location"; - public static final String SELF = "self"; - //public static final ThreadLocal BASE_URI = new ThreadLocal(); - - private static final Logger LOG = LoggerFactory.getLogger( - RepositoryRestController.class); - private static final TypeDescriptor STRING_ARRAY_TYPE = TypeDescriptor.valueOf(String[].class); - - /** - * We manage a list of possible {@link ConversionService}s to handle converting objects in the controller. This list - * is prioritized as well, so one can add a ConversionService at index 0 to make sure that ConversionService takes - * priority whenever an object of the type it can convert is needing conversion. - */ - private DelegatingConversionService conversionService = new DelegatingConversionService( - new DefaultFormattingConversionService() - ); - private MethodParameterConversionService methodParameterConversionService = new MethodParameterConversionService( - conversionService - ); - /** - * Converters for reading and writing representations of objects. - */ - private List httpMessageConverters = new ArrayList(); - /** - * List of {@link MediaType}s we can support, given the list of {@link HttpMessageConverter}s currently configured. - */ - private SortedSet availableMediaTypes = new TreeSet(); - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; - private ObjectMapper objectMapper = new ObjectMapper(); - private RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter; - private UriToDomainObjectUriResolver domainObjectResolver; - private ApplicationContext applicationContext; - - { - List httpMessageConverters = new ArrayList(); - httpMessageConverters.add(new UriListHttpMessageConverter()); - - setHttpMessageConverters(httpMessageConverters); - } - - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * Get the {@link ConversionService} in use by the controller. - * - * @return The internal {@link ConversionService}s. - */ - public ConversionService getConversionService() { - return conversionService; - } - - /** - * Add these {@link ConversionService}s to the list of those being delegated to by the internal {@link - * DelegatingConversionService}. Although this method does an 'add', it is called 'set' to make it JavaBean-friendly. - * - * @param conversionServices - */ - @Autowired(required = false) - public void setConversionServices(List conversionServices) { - if(null == conversionServices) { - return; - } - Collections.reverse(conversionServices); - if(null != conversionService) { - this.conversionService.addConversionServices( - conversionServices.toArray(new ConversionService[conversionServices.size()]) - ); - } - } - - /** - * @return The internal {@link ConversionService}. - * - * @see org.springframework.data.rest.webmvc.RepositoryRestController#getConversionService() - */ - public ConversionService conversionService() { - return conversionService; - } - - /** - * @param conversionServices - * - * @return @this - * - * @see RepositoryRestController#setConversionServices(java.util.List) - */ - public RepositoryRestController conversionServices(List conversionServices) { - setConversionServices(conversionServices); - return this; - } - - /** - * Get the list of default {@link HttpMessageConverter}s. - * - * @return Default converters. - */ - public List getHttpMessageConverters() { - return httpMessageConverters; - } - - /** - * Set the list of available {@link HttpMessageConverter}s, clobbering the defaults. This does not, however, affect - * those user-defined converters that come from the {@link RepositoryRestConfiguration}. - * - * @param httpMessageConverters - */ - @SuppressWarnings({"unchecked"}) - public void setHttpMessageConverters(List httpMessageConverters) { - Assert.notNull(httpMessageConverters); - this.httpMessageConverters = httpMessageConverters; - this.availableMediaTypes.clear(); - for(HttpMessageConverter conv : httpMessageConverters) { - for(MediaType mt : (List)conv.getSupportedMediaTypes()) { - availableMediaTypes.add(mt.toString()); - } - } - for(HttpMessageConverter conv : config.getCustomConverters()) { - for(MediaType mt : (List)conv.getSupportedMediaTypes()) { - availableMediaTypes.add(mt.toString()); - } - } - } - - /** - * @return @this - * - * @see org.springframework.data.rest.webmvc.RepositoryRestController#getHttpMessageConverters() - */ - public List httpMessageConverters() { - return httpMessageConverters; - } - - /** - * @param httpMessageConverters - * - * @return @this - * - * @see RepositoryRestController#setHttpMessageConverters(java.util.List) - */ - public RepositoryRestController httpMessageConverters(List httpMessageConverters) { - setHttpMessageConverters(httpMessageConverters); - return this; - } - - /** - * Get the configuration currently in use. - * - * @return Either the user-defined configuration or a default. - */ - public RepositoryRestConfiguration getRepositoryRestConfig() { - return config; - } - - /** - * Set the configuration this controller will use to inflence its behavior. - * - * @param config - * - * @return @this - */ - @Autowired(required = false) - public RepositoryRestController setRepositoryRestConfig(RepositoryRestConfiguration config) { - this.config = config; - return this; - } - - public RepositoryAwareMappingHttpMessageConverter getMappingHttpMessageConverter() { - return mappingHttpMessageConverter; - } - - @Autowired - public RepositoryRestController setMappingHttpMessageConverter(RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter) { - this.mappingHttpMessageConverter = mappingHttpMessageConverter; - httpMessageConverters.add(mappingHttpMessageConverter); - this.objectMapper = mappingHttpMessageConverter.getObjectMapper(); - return this; - } - - public UriToDomainObjectUriResolver getDomainObjectResolver() { - return domainObjectResolver; - } - - @Autowired - public RepositoryRestController setDomainObjectResolver(UriToDomainObjectUriResolver domainObjectResolver) { - this.domainObjectResolver = domainObjectResolver; - return this; - } - - @SuppressWarnings({"unchecked"}) - @Override public void afterPropertiesSet() throws Exception { - for(ConversionService cs : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, - ConversionService.class) - .values()) { - conversionService.addConversionServices(cs); - } - - GenericConversionService entityConverters = new GenericConversionService(); - for(RepositoryExporter exp : repositoryExporters()) { - for(String repoName : (Set)exp.repositoryNames()) { - RepositoryMetadata repoMeta = exp.repositoryMetadataFor(repoName); - Class domainType = repoMeta.domainType(); - entityConverters.addConverter(domainType, Resource.class, new EntityToResourceConverter(config, repoMeta)); - } - } - conversionService.addConversionServices(entityConverters); - } - - /** - * List available {@link CrudRepository}s that are being exported. - * - * @param request - * @param baseUri - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity listRepositories(ServletServerHttpRequest request, - URI baseUri) throws IOException { - List links = new ArrayList(); - for(RepositoryExporter repoExporter : repositoryExporters) { - for(String name : (Set)repoExporter.repositoryNames()) { - RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor(name); - String rel = repoMeta.rel(); - URI path = buildUri(baseUri, name); - links.add(new Link(path.toString(), rel)); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resources(Collections.emptyList(), links)); - } - - /** - * List entities of a {@link CrudRepository} by invoking - * {@link org.springframework.data.repository.CrudRepository#findAll()} - * and applying any available paging parameters. - * - * @param request - * @param pageSort - * @param baseUri - * @param repository - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity listEntities(ServletServerHttpRequest request, - PagingAndSorting pageSort, - URI baseUri, - @PathVariable String repository) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.FIND_ALL)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - List links = new ArrayList(); - PagedResources.PageMetadata pageMeta = null; - - Iterator allEntities = Collections.emptyList().iterator(); - if(repoMeta.repository() instanceof PagingAndSortingRepository) { - Page page = ((PagingAndSortingRepository)repoMeta.repository()).findAll(pageSort); - if(page.hasContent()) { - allEntities = page.iterator(); - } - - // Set page counts in the response - pageMeta = new PagedResources.PageMetadata( - page.getSize(), - page.getNumber() + 1, - page.getTotalElements(), - page.getTotalPages() - ); - - // Copy over parameters - UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository); - for(String name : request.getServletRequest().getParameterMap().keySet()) { - if(notPagingParam(name)) { - selfUri.queryParam(name, request.getServletRequest().getParameter(name)); - } - } - - // Add next/prev links as necessary - URI nextPrevBase = selfUri.build().toUri(); - maybeAddPrevNextLink( - nextPrevBase, - repoMeta, - pageSort, - page, - !page.isFirstPage() && page.hasPreviousPage(), - page.getNumber(), - "prev", - links - ); - maybeAddPrevNextLink( - nextPrevBase, - repoMeta, - pageSort, - page, - !page.isLastPage() && page.hasNextPage(), - page.getNumber() + 2, - "next", - links - ); - } else { - Iterable it = repoMeta.repository().findAll(); - if(null != it) { - allEntities = it.iterator(); - } - } - - List allResources = new ArrayList(); - while(allEntities.hasNext()) { - Object o = allEntities.next(); - if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { - Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o); - URI selfUri = buildUri(baseUri, repository, id.toString()); - links.add(new Link(selfUri.toString(), - repoMeta.rel() + "." + o.getClass().getSimpleName())); - } else { - allResources.add(o); - } - } - - if(!repoMeta.queryMethods().isEmpty()) { - links.add(new Link(buildUri(baseUri, repository, "search").toString(), - repoMeta.rel() + ".search")); - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - (null != pageMeta - ? new PagedResources(allResources, pageMeta, links) - : new Resources(allResources, links))); - } - - /** - * List the URIs of query methods found on this repository interface. - * - * @param request - * @param baseUri - * @param repository - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/search", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity listQueryMethods(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - Set links = new HashSet(); - - for(Map.Entry entry : ((Map)repoMeta.queryMethods()) - .entrySet()) { - URI baseSearchUri = buildUri(baseUri, repository, "search"); - - // Check for customized rel and path - Method m = entry.getValue().method(); - if(m.isAnnotationPresent(RestResource.class)) { - RestResource resourceAnno = m.getAnnotation(RestResource.class); - links.add(new Link( - buildUri(baseSearchUri, - (StringUtils.hasText(resourceAnno.path()) - ? resourceAnno.path() - : entry.getKey())).toString(), - (StringUtils.hasText(resourceAnno.rel()) - ? repoMeta.rel() + "." + resourceAnno.rel() - : repoMeta.rel() + "." + entry.getKey()) - )); - } else { - // No customizations, use the default - links.add(new Link(buildUri(baseSearchUri, entry.getKey()).toString(), - repoMeta.rel() + "." + entry.getKey())); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resources(Collections.emptyList(), links)); - } - - /** - * Invoke a custom query method on a repository and page the results based on URL parameters supplied by the user or - * the default page size. - * - * @param request - * @param pageSort - * @param baseUri - * @param repository - * @param query - * - * @return - * - * @throws InvocationTargetException - * @throws IllegalAccessException - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/search/{query}", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity query(ServletServerHttpRequest request, - PagingAndSorting pageSort, - URI baseUri, - @PathVariable String repository, - @PathVariable String query) throws InvocationTargetException, - IllegalAccessException, - IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - Repository repo = repoMeta.repository(); - RepositoryQueryMethod queryMethod = repoMeta.queryMethod(query); - if(null == queryMethod) { - return notFoundResponse(request); - } - - Class[] paramTypes = queryMethod.paramTypes(); - String[] paramNames = queryMethod.paramNames(); - Object[] paramVals = new Object[paramTypes.length]; - for(int i = 0; i < paramVals.length; i++) { - if(Pageable.class.isAssignableFrom(paramTypes[i])) { - // Handle paging - paramVals[i] = pageSort; - continue; - } else if(Sort.class.isAssignableFrom(paramTypes[i])) { - // Handle sorting - paramVals[i] = (null != pageSort ? pageSort.getSort() : null); - continue; - } - - String[] queryVals; - if(null == (queryVals = request.getServletRequest().getParameterValues(paramNames[i]))) { - continue; - } - - MethodParameter methodParam = new MethodParameter(queryMethod.method(), i); - String firstVal = (queryVals.length > 0 ? queryVals[0] : null); - if(hasRepositoryMetadataFor(paramTypes[i])) { - RepositoryMetadata paramRepoMeta = repositoryMetadataFor(paramTypes[i]); - // Complex parameter is a managed type - Serializable id = stringToSerializable(firstVal, - (Class)paramRepoMeta.entityMetadata() - .idAttribute() - .type()); - Object o = paramRepoMeta.repository().findOne(id); - if(null == o) { - return notFoundResponse(request); - } - - paramVals[i] = o; - } else if(String.class.isAssignableFrom(paramTypes[i])) { - // Param type is a String - paramVals[i] = firstVal; - } else if(methodParameterConversionService.canConvert(STRING_ARRAY_TYPE, methodParam)) { - // There's a converter from String[] -> param type - paramVals[i] = methodParameterConversionService.convert(queryVals, STRING_ARRAY_TYPE, methodParam); - } else { - // Param type isn't a "simple" type or no converter exists, try JSON - try { - paramVals[i] = objectMapper.readValue(firstVal, paramTypes[i]); - } catch(IOException e) { - throw new IllegalArgumentException(e); - } - } - } - - Object result; - if(null == (result = queryMethod.method().invoke(repo, paramVals))) { - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resources(Collections.emptyList())); - } - - List links = new ArrayList(); - PagedResources.PageMetadata pageMetadata = null; - - Iterator entities = Collections.emptyList().iterator(); - if(result instanceof Collection) { - entities = ((Collection)result).iterator(); - } else if(result instanceof Page) { - Page page = (Page)result; - - if(page.hasContent()) { - entities = page.iterator(); - } - - // Set page counts in the response - pageMetadata = new PagedResources.PageMetadata(page.getSize(), - page.getNumber() + 1, - page.getTotalElements(), - page.getTotalPages()); - - // Copy over parameters - UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository, "search", query); - for(String name : request.getServletRequest().getParameterMap().keySet()) { - if(notPagingParam(name)) { - selfUri.queryParam(name, request.getServletRequest().getParameter(name)); - } - } - - // Add next/prev links as necessary - URI nextPrevBase = selfUri.build().toUri(); - maybeAddPrevNextLink( - nextPrevBase, - repoMeta, - pageSort, - page, - !page.isFirstPage() && page.hasPreviousPage(), - page.getNumber(), - "prev", - links - ); - maybeAddPrevNextLink( - nextPrevBase, - repoMeta, - pageSort, - page, - !page.isLastPage() && page.hasNextPage(), - page.getNumber() + 2, - "next", - links - ); - } else { - entities = Collections.singletonList(result).iterator(); - } - - List results = new ArrayList(); - while(entities.hasNext()) { - Object obj = entities.next(); - if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { - if(!hasRepositoryMetadataFor(obj.getClass())) { - results.add(obj); - continue; - } - // This object is managed by a repository - RepositoryMetadata elemRepoMeta = repositoryMetadataFor(obj.getClass()); - String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString(); - String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); - URI path = buildUri(baseUri, repository, id); - links.add(new Link(path.toString(), rel)); - } else { - results.add(obj); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - (null != pageMetadata - ? new PagedResources(results, pageMetadata, links) - : new Resources(results, links))); - } - - /** - * Create a new entity by reading the incoming data and calling {@link CrudRepository#save(Object)} and letting the - * ID be auto-generated. - *

- * To get the entity back in the body of the response, simpy add the URL parameter

returnBody=true
. - * - * @param request - * @param baseUri - * @param repository - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}", - method = RequestMethod.POST - ) - @ResponseBody - public ResponseEntity create(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - CrudRepository repo = repoMeta.repository(); - - MediaType incomingMediaType = request.getHeaders().getContentType(); - Object incoming = readIncoming(request, incomingMediaType, repoMeta.entityMetadata().type()); - if(null == incoming) { - throw new HttpMessageNotReadableException("Could not create an instance of " + repoMeta.entityMetadata() - .type() - .getSimpleName() + " from input."); - } - - publishEvent(new BeforeSaveEvent(incoming)); - Object savedEntity = repo.save(incoming); - publishEvent(new AfterSaveEvent(savedEntity)); - - String sId = repoMeta.entityMetadata().idAttribute().get(savedEntity).toString(); - URI selfUri = buildUri(baseUri, repository, sId); - - HttpHeaders headers = new HttpHeaders(); - headers.set(LOCATION, selfUri.toString()); - - Resource body = null; - if(returnBody(request)) { - body = new Resource(savedEntity); - } - - return negotiateResponse(request, HttpStatus.CREATED, headers, body); - } - - /** - * Retrieve a specific entity. - * - * @param request - * @param baseUri - * @param repository - * @param id - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity entity(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository, - @PathVariable String id) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - CrudRepository repo = repoMeta.repository(); - Object entity = repo.findOne(serId); - if(null == entity) { - return notFoundResponse(request); - } - - HttpHeaders headers = new HttpHeaders(); - if(null != repoMeta.entityMetadata().versionAttribute()) { - Object version = repoMeta.entityMetadata().versionAttribute().get(entity); - if(null != version) { - List etags = request.getHeaders().getIfNoneMatch(); - for(String etag : etags) { - if(("\"" + version.toString() + "\"").equals(etag)) { - return negotiateResponse(request, HttpStatus.NOT_MODIFIED, new HttpHeaders(), null); - } - } - headers.set("ETag", "\"" + version.toString() + "\""); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - headers, - entity); - } - - /** - * Create an entity with a specific ID or update an existing entity. - * - * @param request - * @param baseUri - * @param repository - * @param id - * - * @return - * - * @throws IOException - * @throws IllegalAccessException - * @throws InstantiationException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}", - method = { - RequestMethod.PUT - } - ) - @ResponseBody - public ResponseEntity createOrUpdate(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository, - @PathVariable String id) throws IOException, - IllegalAccessException, - InstantiationException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE) || !repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - CrudRepository repo = repoMeta.repository(); - Class domainType = repoMeta.entityMetadata().type(); - - MediaType incomingMediaType = request.getHeaders().getContentType(); - Object incoming; - if(null == (incoming = readIncoming(request, incomingMediaType, domainType))) { - throw new HttpMessageNotReadableException("Could not create an instance of " - + domainType.getSimpleName() + " from input."); - } - // Set the ID specified in the URL - repoMeta.entityMetadata().idAttribute().set(serId, incoming); - - boolean isUpdate = false; - Object entity; - if(null != (entity = repo.findOne(serId))) { - // Updating an existing resource - isUpdate = true; - List attrs = new ArrayList(); - attrs.addAll(repoMeta.entityMetadata().embeddedAttributes().values()); - attrs.addAll(repoMeta.entityMetadata().linkedAttributes().values()); - for(AttributeMetadata attrMeta : attrs) { - Object incomingVal; - if(null != (incomingVal = attrMeta.get(incoming))) { - attrMeta.set(incomingVal, entity); - } - } - } else { - entity = incoming; - } - - publishEvent(new BeforeSaveEvent(entity)); - Object savedEntity = repo.save(entity); - publishEvent(new AfterSaveEvent(savedEntity)); - - URI selfUri = buildUri(baseUri, repository, id); - - Object body = null; - if(returnBody(request)) { - body = savedEntity; - } - - if(!isUpdate) { - HttpHeaders headers = new HttpHeaders(); - headers.set(LOCATION, selfUri.toString()); - - return negotiateResponse(request, - HttpStatus.CREATED, - headers, - body); - } else { - return negotiateResponse(request, - (null != body ? HttpStatus.OK : HttpStatus.NO_CONTENT), - new HttpHeaders(), - body); - } - - } - - /** - * Delete an entity. - * - * @param request - * @param repository - * @param id - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}", - method = RequestMethod.DELETE - ) - @ResponseBody - public ResponseEntity deleteEntity(ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.DELETE_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - CrudRepository repo = repoMeta.repository(); - Object entity; - if(null == (entity = repo.findOne(serId))) { - return notFoundResponse(request); - } - - publishEvent(new BeforeDeleteEvent(entity)); - repo.delete(serId); - publishEvent(new AfterDeleteEvent(entity)); - - return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); - } - - /** - * Retrieve the property of an entity. - * - * @param request - * @param baseUri - * @param repository - * @param id - * @param property - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}/{property}", - method = RequestMethod.GET - ) - @ResponseBody - public ResponseEntity propertyOfEntity(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property) throws IOException { - String accept = request.getServletRequest().getHeader("Accept"); - - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - CrudRepository repo = repoMeta.repository(); - - Object entity; - if(null == (entity = repo.findOne(serId))) { - return notFoundResponse(request); - } - - AttributeMetadata attrMeta; - if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { - return notFoundResponse(request); - } - - Class attrType; - if(null == (attrType = attrMeta.elementType())) { - attrType = attrMeta.type(); - } - - RepositoryMetadata propRepoMeta = repositoryMetadataFor(attrType); - if(!propRepoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - Object propVal; - if(null == (propVal = attrMeta.get(entity))) { - return notFoundResponse(request); - } - - Set links = new HashSet(); - AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute(); - String propertyRel = repository + - "." + entity.getClass().getSimpleName() + - "." + property; - if(propVal instanceof Collection) { - propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName(); - - List> outgoing = new ArrayList>(); - for(Object o : (Collection)propVal) { - String propValId = idAttr.get(o).toString(); - URI path = buildUri(baseUri, repository, id, property, propValId); - - if(shouldReturnLinks(accept)) { - links.add(new Link(path.toString(), propertyRel)); - } else { - Resource r; - if(conversionService.canConvert(o.getClass(), Resource.class)) { - r = conversionService.convert(o, Resource.class); - } else { - r = new Resource(o); - } - r.add(new Link(path.toString(), propertyRel)); - outgoing.add(r); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resources(outgoing, links)); - - } else if(propVal instanceof Map) { - propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName(); - Map resource = new HashMap(); - for(Map.Entry entry : ((Map)propVal).entrySet()) { - if(null == entry.getValue()) { - continue; - } - - String propValId = idAttr.get(entry.getValue()).toString(); - URI path = buildUri(baseUri, repository, id, property, propValId); - - Object oKey = entry.getKey(); - String sKey = objectToMapKey(oKey); - - if(shouldReturnLinks(accept)) { - resource.put(sKey, new Link(path.toString(), propertyRel)); - } else { - Resource r; - if(conversionService.canConvert(entry.getValue().getClass(), Resource.class)) { - r = conversionService.convert(entry.getValue(), Resource.class); - } else { - r = new Resource(entry.getValue()); - } - r.add(new Link(path.toString(), propertyRel)); - resource.put(sKey, r); - } - } - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resource(resource, links)); - - } else { - URI path = buildUri(baseUri, repository, id, property); - if(shouldReturnLinks(accept)) { - links.add(new Link(path.toString(), propertyRel)); - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - new Resources(Collections.emptyList(), links)); - - } else { - Resource r; - if(conversionService.canConvert(propVal.getClass(), Resource.class)) { - r = conversionService.convert(propVal, Resource.class); - } else { - r = new Resource(propVal); - } - r.add(new Link(path.toString(), propertyRel)); - - return negotiateResponse(request, - HttpStatus.OK, - new HttpHeaders(), - r); - - } - - - } - - } - - /** - * Update the property of an entity if that property is also managed by a {@link CrudRepository}. - * - * @param request - * @param baseUri - * @param repository - * @param id - * @param property - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}/{property}", - method = { - RequestMethod.PUT, - RequestMethod.POST - } - ) - @ResponseBody - public ResponseEntity updatePropertyOfEntity(final ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository, - @PathVariable String id, - final @PathVariable String property) throws IOException { - - final RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - CrudRepository repo = repoMeta.repository(); - - final Object entity; - final AttributeMetadata attrMeta; - if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { - return notFoundResponse(request); - } - - Object linked = attrMeta.get(entity); - final AtomicReference rel = new AtomicReference(); - Handler> entityHandler = new Handler>() { - @Override public ResponseEntity handle(Object linkedEntity) { - - if(attrMeta.isCollectionLike()) { - Collection c = new ArrayList(); - Collection current = attrMeta.asCollection(entity); - if(request.getMethod() == HttpMethod.POST && null != current) { - c.addAll(current); - } - c.add(linkedEntity); - attrMeta.set(c, entity); - } else if(attrMeta.isSetLike()) { - Set s = new HashSet(); - Set current = attrMeta.asSet(entity); - if(request.getMethod() == HttpMethod.POST && null != current) { - s.addAll(current); - } - s.add(linkedEntity); - attrMeta.set(s, entity); - } else if(attrMeta.isMapLike()) { - Map m = new HashMap(); - Map current = attrMeta.asMap(entity); - if(request.getMethod() == HttpMethod.POST && null != current) { - m.putAll(current); - } - String key = rel.get(); - if(null == key) { - throw new IllegalArgumentException("Map key cannot be null (usually the 'rel' value of a JSON object)."); - } - m.put(rel.get(), linkedEntity); - attrMeta.set(m, entity); - } else { - // Don't support POST when it's a single value - if(request.getMethod() == HttpMethod.POST) { - try { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } catch(IOException e) { - throw new IllegalStateException(e.getMessage(), e); - } - } - attrMeta.set(linkedEntity, entity); - } - - return null; - } - }; - - MediaType incomingMediaType = request.getHeaders().getContentType(); - Resource incomingLinks = readIncoming(request, - incomingMediaType, - Resource.class); - for(Link l : incomingLinks.getLinks()) { - Object o; - if(null != (o = domainObjectResolver.resolve(baseUri, URI.create(l.getHref())))) { - rel.set(l.getRel()); - ResponseEntity possibleResponse = entityHandler.handle(o); - if(null != possibleResponse) { - return possibleResponse; - } - } - - publishEvent(new BeforeSaveEvent(entity)); - publishEvent(new BeforeLinkSaveEvent(entity, linked)); - Object savedEntity = repo.save(entity); - linked = attrMeta.get(savedEntity); - publishEvent(new AfterLinkSaveEvent(savedEntity, linked)); - publishEvent(new AfterSaveEvent(savedEntity)); - } - - if(request.getMethod() == HttpMethod.PUT) { - return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); - } else { - return negotiateResponse(request, HttpStatus.CREATED, new HttpHeaders(), null); - } - } - - /** - * Clear all linked entities of a specific property. - * - * @param request - * @param repository - * @param id - * @param property - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}/{property}", - method = { - RequestMethod.DELETE - } - ) - @ResponseBody - public ResponseEntity clearLinks(ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - CrudRepository repo = repoMeta.repository(); - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - - Object entity; - AttributeMetadata attrMeta; - if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { - return notFoundResponse(request); - } - - // Check if this is a nullable (optional) relationship and if not, fail with a 405 Method Not Allowed - if(!attrMeta.isNullable()) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - Object linked = attrMeta.get(entity); - attrMeta.set(null, entity); - - publishEvent(new BeforeLinkSaveEvent(entity, linked)); - Object savedEntity = repo.save(entity); - publishEvent(new AfterLinkSaveEvent(savedEntity, null)); - - return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); - } - - /** - * Retrieve a linked entity from a parent entity. - * - * @param request - * @param baseUri - * @param repository - * @param id - * @param property - * @param linkedId - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}/{property}/{linkedId}", - method = { - RequestMethod.GET - } - ) - @ResponseBody - public ResponseEntity linkedEntity(ServletServerHttpRequest request, - URI baseUri, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property, - @PathVariable String linkedId) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - // Check for the existence of the parent - CrudRepository repo = repoMeta.repository(); - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - if(!repo.exists(serId)) { - return notFoundResponse(request); - } - - // Check for the existence of the property - AttributeMetadata attrMeta; - if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { - return notFoundResponse(request); - } - - // Check for the existence of a Repository for the linked entity - RepositoryMetadata linkedRepoMeta; - if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) { - return notFoundResponse(request); - } - - if(!linkedRepoMeta.exportsMethod(CrudMethod.FIND_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - // Find the linked entity - CrudRepository linkedRepo = linkedRepoMeta.repository(); - Serializable sChildId = stringToSerializable(linkedId, - (Class)linkedRepoMeta.entityMetadata() - .idAttribute() - .type()); - Object linkedEntity; - if(null == (linkedEntity = linkedRepo.findOne(sChildId))) { - return notFoundResponse(request); - } - - String propertyRel = repoMeta.rel() + "." + repoMeta.entityMetadata().type().getSimpleName() + "." + property; - URI propertyPath = buildUri(baseUri, repository, id, property, linkedId); - URI selfUri = buildUri(baseUri, linkedRepoMeta.name(), linkedId); - - Resource r; - if(conversionService.canConvert(linkedEntity.getClass(), Resource.class)) { - r = conversionService.convert(linkedEntity, Resource.class); - } else { - r = new Resource(linkedEntity); - } - r.add(new Link(propertyPath.toString(), propertyRel)); - - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Location", selfUri.toString()); - - return negotiateResponse(request, HttpStatus.OK, headers, r); - } - - /** - * Delete a specific relationship between a child entity and its parent. - * - * @param request - * @param repository - * @param id - * @param property - * @param linkedId - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = "/{repository}/{id}/{property}/{linkedId}", - method = { - RequestMethod.DELETE - } - ) - @ResponseBody - public ResponseEntity deleteLink(ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property, - @PathVariable String linkedId) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - CrudRepository repo = repoMeta.repository(); - // If I can't load the parent entity, then this method isn't allowed. - if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE) || !repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - Serializable serId = stringToSerializable(id, - (Class)repoMeta.entityMetadata() - .idAttribute() - .type()); - Object entity; - AttributeMetadata attrMeta; - if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { - return notFoundResponse(request); - } - - // Check if this is a nullable (optional) relationship and if not, fail with a 405 Method Not Allowed - if(!attrMeta.isNullable()) { - return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); - } - - // Find linked entity - RepositoryMetadata linkedRepoMeta; - if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) { - return notFoundResponse(request); - } - - CrudRepository linkedRepo = linkedRepoMeta.repository(); - Serializable sChildId = stringToSerializable(linkedId, - (Class)linkedRepoMeta.entityMetadata() - .idAttribute() - .type()); - - Object linkedEntity; - if(null == (linkedEntity = linkedRepo.findOne(sChildId))) { - return notFoundResponse(request); - } - - // Remove linked entity from relationship based on property type - if(attrMeta.isCollectionLike()) { - Collection c = attrMeta.asCollection(entity); - if(null != c && c != Collections.emptyList()) { - c.remove(linkedEntity); - } - } else if(attrMeta.isSetLike()) { - Set s = attrMeta.asSet(entity); - if(null != s && s != Collections.emptySet()) { - s.remove(linkedEntity); - } - } else if(attrMeta.isMapLike()) { - Object keyToRemove = null; - Map m = attrMeta.asMap(entity); - if(null != m && m != Collections.emptyMap()) { - for(Map.Entry entry : m.entrySet()) { - Object val = entry.getValue(); - if(null != val && val.equals(linkedEntity)) { - keyToRemove = entry.getKey(); - break; - } - } - if(null != keyToRemove) { - m.remove(keyToRemove); - } - } - } else { - attrMeta.set(null, entity); - } - - publishEvent(new BeforeLinkDeleteEvent(entity, linkedEntity)); - Object savedEntity = repo.save(entity); - publishEvent(new AfterLinkDeleteEvent(savedEntity, linkedEntity)); - - return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); - } - - /** - * Send a 404 if no repository was found. - * - * @param e - * @param request - * - * @return - * - * @throws IOException - */ - @ExceptionHandler(RepositoryNotFoundException.class) - @ResponseBody - public ResponseEntity handleRepositoryNotFoundFailure(RepositoryNotFoundException e, - ServletServerHttpRequest request) throws IOException { - if(LOG.isWarnEnabled()) { - LOG.warn("RepositoryNotFoundException: " + e.getMessage()); - } - return notFoundResponse(request); - } - - /** - * Handle NPEs as a regular 500 error. - * - * @param e - * @param request - * - * @return - * - * @throws IOException - */ - @ExceptionHandler(NullPointerException.class) - @ResponseBody - public ResponseEntity handleNPE(NullPointerException e, - ServletServerHttpRequest request) throws IOException { - LOG.error(e.getMessage(), e); - return errorResponse(request, HttpStatus.INTERNAL_SERVER_ERROR, e); - } - - /** - * Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type. - * - * @param t - * @param request - * - * @return - * - * @throws IOException - */ - @ExceptionHandler( - { - InvocationTargetException.class, - IllegalArgumentException.class, - ClassCastException.class, - ConversionFailedException.class - } - ) - @ResponseBody - public ResponseEntity handleMiscFailures(Throwable t, - ServletServerHttpRequest request) throws IOException { - LOG.error(t.getMessage(), t); - return errorResponse(request, HttpStatus.BAD_REQUEST, t); - } - - /** - * Send a 409 Conflict in case of concurrent modification. - * - * @param ex - * @param request - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @ExceptionHandler({OptimisticLockingFailureException.class, DataIntegrityViolationException.class}) - @ResponseBody - public ResponseEntity handleConflict(Exception ex, - ServletServerHttpRequest request) throws IOException { - LOG.error(ex.getMessage(), ex); - return errorResponse(request, HttpStatus.CONFLICT, ex); - } - - /** - * Send a 400 Bad Request in case of a validation failure. - * - * @param ex - * @param request - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @ExceptionHandler(RepositoryConstraintViolationException.class) - @ResponseBody - public ResponseEntity handleValidationFailure(RepositoryConstraintViolationException ex, - ServletServerHttpRequest request) throws IOException { - LOG.error(ex.getMessage(), ex); - - Map m = new HashMap(); - List errors = new ArrayList(); - for(FieldError fe : ex.getErrors().getFieldErrors()) { - List args = new ArrayList(); - args.add(fe.getObjectName()); - args.add(fe.getField()); - args.add(fe.getRejectedValue()); - if(null != fe.getArguments()) { - for(Object o : fe.getArguments()) { - args.add(o); - } - } - - String msg = applicationContext.getMessage(fe.getCode(), - args.toArray(), - fe.getDefaultMessage(), - null); - errors.add(msg); - } - m.put("errors", errors); - - return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), m); - } - - /** - * Send a 400 Bad Request in case of a validation failure. - * - * @param ex - * @param request - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @ExceptionHandler(ConstraintViolationException.class) - @ResponseBody - public ResponseEntity handleJsr303ValidationFailure(ConstraintViolationException ex, - ServletServerHttpRequest request) throws IOException { - LOG.error(ex.getMessage(), ex); - - Map m = new HashMap(); - List errors = new ArrayList(); - for(ConstraintViolation cv : ex.getConstraintViolations()) { - String msg = applicationContext.getMessage(cv.getMessageTemplate(), - new Object[]{ - cv.getLeafBean().getClass().getSimpleName(), - cv.getPropertyPath().toString(), - cv.getInvalidValue() - }, - cv.getMessage(), - null); - errors.add(msg); - } - m.put("errors", errors); - - return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), m); - } - - /** - * Send a 400 Bad Request in case no converter was found to process the input or output. - * - * @param ex - * @param request - * - * @return - * - * @throws IOException - */ - @SuppressWarnings({"unchecked"}) - @ExceptionHandler({HttpMessageNotReadableException.class, HttpMessageNotWritableException.class}) - @ResponseBody - public ResponseEntity handleMessageConversionFailure(Exception ex, - HttpServletRequest request) throws IOException { - LOG.error(ex.getMessage(), ex); - - Map m = new HashMap(); - m.put("message", ex.getMessage()); - m.put("acceptableTypes", availableMediaTypes); - - return negotiateResponse(new ServletServerHttpRequest(request), HttpStatus.BAD_REQUEST, new HttpHeaders(), m); - } - - /* - ----------------------------------- - Internal helper methods - ----------------------------------- - */ - - @SuppressWarnings({"unchecked"}) - private void maybeAddPrevNextLink(URI resourceUri, - RepositoryMetadata repoMeta, - PagingAndSorting pageSort, - Page page, - boolean addIf, - int nextPage, - String rel, - Collection links) { - if(null != page && addIf) { - UriComponentsBuilder urib = UriComponentsBuilder.fromUri(resourceUri); - urib.queryParam(config.getPageParamName(), nextPage); // PageRequest is 0-based, so it's already (page - 1) - urib.queryParam(config.getLimitParamName(), page.getSize()); - pageSort.addSortParameters(urib); - links.add(new Link(urib.build().toUri().toString(), repoMeta.rel() + "." + rel)); - } - } - - @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 { - // Check custom converters first - for(HttpMessageConverter conv : config.getCustomConverters()) { - if(conv.canRead(targetType, incomingMediaType)) { - return (V)conv.read(targetType, request); - } - } - // Use our always-available default list of converters - for(HttpMessageConverter conv : httpMessageConverters) { - if(conv.canRead(targetType, incomingMediaType)) { - return (V)conv.read(targetType, request); - } - } - return (V)mappingHttpMessageConverter.read(targetType, request); - } - - // private Set extractLinkedProperties(String repoRel, - // Object entity, - // EntityMetadata entityMetadata, - // URI baseUri) { - // Set links = new HashSet(); - // for(String attrName : entityMetadata.linkedAttributes().keySet()) { - // URI uri = buildUri(baseUri, attrName); - // String rel = repoRel + "." + entity.getClass().getSimpleName() + "." + attrName; - // links.add(new Link(uri.toString(), rel)); - // } - // return links; - // } - // - // private Map extractEmbeddedProperties(String repoRel, - // Object entity, - // EntityMetadata entityMetadata, - // URI baseUri) { - // Map entityDto = new HashMap(); - // for(Map.Entry attrMeta : entityMetadata.embeddedAttributes().entrySet()) { - // String name = attrMeta.getKey(); - // Object val; - // if(null != (val = attrMeta.getValue().get(entity))) { - // entityDto.put(name, val); - // } - // } - // return entityDto; - // } - - private boolean shouldReturnLinks(String acceptHeader) { - if(null != acceptHeader) { - List accept = MediaType.parseMediaTypes(acceptHeader); - for(MediaType mt : accept) { - if(mt.getSubtype().startsWith("x-spring-data-verbose")) { - return false; - } else if(mt.getSubtype().startsWith("x-spring-data-compact")) { - return true; - } else if(mt.getSubtype().equals("uri-list")) { - return true; - } - } - } - return false; - } - - private boolean returnBody(ServletServerHttpRequest request) { - String s = request.getServletRequest().getParameter("returnBody"); - if(null != s) { - return "true".equals(s); - } else { - return false; - } - } - - private void publishEvent(E event) { - if(null != applicationContext) { - applicationContext.publishEvent(event); - } - } - - private boolean notPagingParam(String name) { - return (!config.getPageParamName().equals(name) - && !config.getLimitParamName().equals(name) - && !config.getSortParamName().equals(name)); - } - - @SuppressWarnings({"unchecked"}) - private Map throwableToMap(Throwable t) { - Map m = new HashMap(); - m.put("message", t.getMessage()); - if(null != t.getCause()) { - m.put("cause", throwableToMap(t.getCause())); - } - return m; - } - - private String objectToMapKey(Object obj) { - Assert.notNull(obj, "Map key cannot be null!"); - - RepositoryMetadata repoMeta; - String key; - if(ClassUtils.isAssignable(obj.getClass(), String.class)) { - key = (String)obj; - } else if(null != (repoMeta = repositoryMetadataFor(obj.getClass()))) { - AttributeMetadata attrMeta = repoMeta.entityMetadata().idAttribute(); - String id = attrMeta.get(obj).toString(); - key = "@" + buildUri(config.getBaseUri(), repoMeta.name(), id); - } else { - key = conversionService.convert(obj, String.class); - } - - return key; - } - - private ResponseEntity notFoundResponse(ServletServerHttpRequest request) throws IOException { - return negotiateResponse(request, HttpStatus.NOT_FOUND, new HttpHeaders(), null); - } - - @SuppressWarnings({"unchecked"}) - private ResponseEntity errorResponse(ServletServerHttpRequest request, HttpStatus status, Throwable t) - throws IOException { - Object body = null; - if(config.isDumpErrors()) { - body = throwableToMap(t); - } - return negotiateResponse(request, status, new HttpHeaders(), body); - } - - @SuppressWarnings({"unchecked"}) - private ResponseEntity negotiateResponse(final ServletServerHttpRequest request, - final HttpStatus status, - final HttpHeaders headers, - final Object resource) throws IOException { - - String jsonpParam = request.getServletRequest().getParameter(config.getJsonpParamName()); - String jsonpOnErrParam = null; - if(null != config.getJsonpOnErrParamName()) { - jsonpOnErrParam = request.getServletRequest().getParameter(config.getJsonpOnErrParamName()); - } - - if(null == resource) { - return maybeWrapJsonp(status, jsonpParam, jsonpOnErrParam, headers, null); - } - - MediaType acceptType = config.getDefaultMediaType(); - HttpMessageConverter converter = findWriteConverter(resource.getClass(), acceptType); - // If an Accept header is specified that isn't the catch-all, try and find a converter for it. - if(null == converter) { - for(MediaType mt : request.getHeaders().getAccept()) { - if(MediaType.ALL.equals(mt)) { - continue; - } - - HttpMessageConverter hmc; - if(null == (hmc = findWriteConverter(resource.getClass(), mt))) { - continue; - } - - if(!"*".equals(mt.getSubtype())) { - acceptType = mt; - headers.setContentType(acceptType); - converter = hmc; - } - break; - } - } - - if(null == converter) { - converter = mappingHttpMessageConverter; - headers.setContentType(MediaType.APPLICATION_JSON); - } - - final ByteArrayOutputStream bout = new ByteArrayOutputStream(); - converter.write(resource, headers.getContentType(), new HttpOutputMessage() { - @Override public OutputStream getBody() throws IOException { - return bout; - } - - @Override public HttpHeaders getHeaders() { - return headers; - } - }); - - return maybeWrapJsonp(status, jsonpParam, jsonpOnErrParam, headers, bout.toByteArray()); - } - - @SuppressWarnings({"unchecked"}) - private HttpMessageConverter findWriteConverter(Class type, MediaType mediaType) { - for(HttpMessageConverter conv : config.getCustomConverters()) { - if(conv.canWrite(type, mediaType)) { - return conv; - } - } - for(HttpMessageConverter conv : httpMessageConverters) { - if(conv.canWrite(type, mediaType)) { - return conv; - } - } - return null; - } - - private ResponseEntity maybeWrapJsonp(HttpStatus status, - String jsonpParam, - String jsonpOnErrParam, - HttpHeaders headers, - byte[] body) { - - byte[] responseBody = (null == body ? new byte[0] : body); - if(status.value() >= 400 && null != jsonpOnErrParam) { - status = HttpStatus.OK; - responseBody = String.format("%s(%s, %s)", - jsonpOnErrParam, - status.value(), - (null != body ? new String(body) : null)) - .getBytes(); - headers.setContentType(MediaTypes.APPLICATION_JAVASCRIPT); - } else if(null != jsonpParam) { - responseBody = String.format("%s(%s)", - jsonpParam, - (null != body ? new String(body) : null)) - .getBytes(); - headers.setContentType(MediaTypes.APPLICATION_JAVASCRIPT); - } - headers.setContentLength(responseBody.length); - - return new ResponseEntity(responseBody, headers, status); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestDispatcherServlet.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestDispatcherServlet.java new file mode 100644 index 000000000..828fc7788 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestDispatcherServlet.java @@ -0,0 +1,19 @@ +package org.springframework.data.rest.webmvc; + +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Special {@link DispatcherServlet} subclass that certain exporter components can recognize. + * + * @author Jon Brisbin + */ +public class RepositoryRestDispatcherServlet extends DispatcherServlet { + public RepositoryRestDispatcherServlet(WebApplicationContext webApplicationContext) { + super(webApplicationContext); + setContextClass(AnnotationConfigWebApplicationContext.class); + setContextConfigLocation(RepositoryRestMvcConfiguration.class.getName()); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java deleted file mode 100644 index 07b9aeb0a..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * Convenience {@link DispatcherServlet} that sets the 'contextClass' and 'contextConfigLocation' properties to the - * correct values for using the REST exporter in a web.xml file. - * - * @author Jon Brisbin - */ -public class RepositoryRestExporterServlet extends DispatcherServlet { - - private static final long serialVersionUID = 1L; - - public RepositoryRestExporterServlet() { - configure(); - } - - public RepositoryRestExporterServlet(WebApplicationContext webApplicationContext) { - super(webApplicationContext); - configure(); - } - - private void configure() { - setContextClass(AnnotationConfigWebApplicationContext.class); - setContextConfigLocation(RepositoryRestMvcConfiguration.class.getName()); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index ca87cbd6f..67713a800 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -4,15 +4,14 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; -import org.springframework.data.rest.webmvc.json.JsonSchemaController; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** * {@link RequestMappingHandlerAdapter} implementation that adds a couple argument resolvers for controller method - * parameters used in the REST exporter controller. Also only looks for handler methods in the {@link - * RepositoryRestController} class to help isolate this handler adapter from other handler adapters the user might have + * parameters used in the REST exporter controller. Also only looks for handler methods in the Spring Data REST + * provided controller classes to help isolate this handler adapter from other handler adapters the user might have * configured in their Spring MVC context. * * @author Jon Brisbin @@ -32,9 +31,12 @@ public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandl } @Override protected boolean supportsInternal(HandlerMethod handlerMethod) { + Class controllerType = handlerMethod.getBeanType(); return super.supportsInternal(handlerMethod) - && (RepositoryRestController.class.isAssignableFrom(handlerMethod.getBeanType()) - || JsonSchemaController.class.isAssignableFrom(handlerMethod.getBeanType())); + && (RepositoryController.class.isAssignableFrom(controllerType) + || RepositoryEntityController.class.isAssignableFrom(controllerType) + || RepositoryPropertyReferenceController.class.isAssignableFrom(controllerType) + || RepositorySearchController.class.isAssignableFrom(controllerType)); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java index 6399196b1..4e8cfcdea 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java @@ -1,16 +1,23 @@ package org.springframework.data.rest.webmvc; -import java.util.Collections; -import java.util.HashSet; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; +import static org.springframework.util.StringUtils.*; + +import java.util.ArrayList; import java.util.List; -import java.util.Set; +import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; -import org.springframework.data.rest.repository.RepositoryExporter; -import org.springframework.data.rest.webmvc.json.JsonSchemaController; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.http.MediaType; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -26,39 +33,83 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { @Autowired - private EntityManagerFactory entityManagerFactory; - @Autowired(required = false) - private List repositoryExporters = Collections.emptyList(); - private Set repositoryNames = new HashSet(); + private Repositories repositories; + @Autowired + private RepositoryRestConfiguration config; + private EntityManagerFactory entityManagerFactory; public RepositoryRestHandlerMapping() { setOrder(Ordered.LOWEST_PRECEDENCE); } + @PersistenceContext + public void setEntityManager(EntityManager entityManager) { + this.entityManagerFactory = entityManager.getEntityManagerFactory(); + } + @SuppressWarnings({"unchecked"}) @Override - protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { - if(repositoryNames.isEmpty() && !repositoryExporters.isEmpty()) { - for(RepositoryExporter re : repositoryExporters) { - repositoryNames.addAll(re.repositoryNames()); + protected HandlerMethod lookupHandlerMethod(String lookupPath, + HttpServletRequest origRequest) throws Exception { + String acceptType = origRequest.getHeader("Accept"); + List acceptHeaderTypes = MediaType.parseMediaTypes(acceptType); + List acceptableTypes = new ArrayList(); + for(MediaType mt : acceptHeaderTypes) { + if(("*".equals(mt.getType()) && ("*".equals(mt.getSubtype())) + || ("application".equals(mt.getType()) && "*".equals(mt.getSubtype())))) { + mt = config.getDefaultMediaType(); + } + if(!acceptableTypes.contains(mt)) { + acceptableTypes.add(mt); } } - String[] parts = lookupPath.split("/"); - if(parts.length == 0) { - // Root request - return super.lookupHandlerMethod(lookupPath, request); + if(acceptableTypes.size() > 1) { + acceptType = collectionToDelimitedString(acceptableTypes, ","); + } else if(acceptableTypes.size() == 1) { + acceptType = acceptableTypes.get(0).toString(); } else { - if(repositoryNames.contains(parts[1])) { + acceptType = config.getDefaultMediaType().toString(); + } + + HttpServletRequest request = new DefaultAcceptTypeHttpServletRequest(origRequest, acceptType); + + if(acceptType.contains("javascript")) { + if(null != request.getParameter(config.getJsonpParamName()) + || null != request.getParameter(config.getJsonpOnErrParamName())) { return super.lookupHandlerMethod(lookupPath, request); } else { return null; } } + String requestUri = lookupPath; + if(requestUri.startsWith("/")) { + requestUri = requestUri.substring(1); + } + if(!hasText(requestUri)) { + return super.lookupHandlerMethod(lookupPath, request); + } + String[] parts = requestUri.split("/"); + if(parts.length == 0) { + // Root request + return super.lookupHandlerMethod(lookupPath, request); + } + + for(Class domainType : repositories) { + RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType); + ResourceMapping mapping = getResourceMapping(config, repoInfo); + if(mapping.getPath().equals(parts[0]) && mapping.isExported()) { + return super.lookupHandlerMethod(lookupPath, request); + } + } + + return null; } @Override protected boolean isHandler(Class beanType) { - return (RepositoryRestController.class.isAssignableFrom(beanType) - || JsonSchemaController.class.isAssignableFrom(beanType)); + return (RepositoryController.class.isAssignableFrom(beanType) + || RepositoryEntityController.class.isAssignableFrom(beanType) + || RepositoryPropertyReferenceController.class.isAssignableFrom(beanType) + || RepositorySearchController.class.isAssignableFrom(beanType)); } @Override protected void extendInterceptors(List interceptors) { @@ -69,4 +120,22 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { } } + private static class DefaultAcceptTypeHttpServletRequest extends HttpServletRequestWrapper { + private final String defaultAcceptType; + + private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request, + String defaultAcceptType) { + super(request); + this.defaultAcceptType = defaultAcceptType; + } + + @Override public String getHeader(String name) { + if("accept".equals(name.toLowerCase())) { + return defaultAcceptType; + } else { + return super.getHeader(name); + } + } + } + } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java deleted file mode 100644 index 66fb9b3c6..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ /dev/null @@ -1,186 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.util.Arrays; - -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.UriToDomainObjectUriResolver; -import org.springframework.data.rest.repository.context.AnnotatedHandlerBeanPostProcessor; -import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener; -import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter; -import org.springframework.data.rest.webmvc.json.JsonSchemaController; -import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule; -import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; - -/** - * Main Spring MVC configuration for the REST exporter. Can be subclassed and any of these methods overridden to - * provide - * custom configuration for your environment. More than likely, however, it won't be necessary to do this as most - * user-configurable properties are defined on the {@link RepositoryRestConfiguration} bean, which you can define in - * your own ApplicationContext (which can take the form of an XML file in the classpath at location - * 'META-INF/spring-data-rest/' with a name that ends with '-export.xml'). - * - * @author Jon Brisbin - */ -@Configuration -@ImportResource("classpath*:META-INF/spring-data-rest/**/*-export.xml") -public class RepositoryRestMvcConfiguration { - - /** - * {@link org.springframework.data.rest.repository.RepositoryExporter} implementation for exporting JPA repositories. - */ - @Autowired(required = false) - protected JpaRepositoryExporter customJpaRepositoryExporter; - - /** - * {@link org.springframework.context.ApplicationListener} implementation for invoking {@link - * org.springframework.validation.Validator} instances assigned to specific domain types. - */ - @Autowired(required = false) - protected ValidatingRepositoryEventListener validatingRepositoryEventListener; - - /** - * Main configuration for the REST exporter. - */ - @Autowired(required = false) - protected RepositoryRestConfiguration repositoryRestConfig = RepositoryRestConfiguration.DEFAULT; - - /** - * For getting access to the {@link javax.persistence.EntityManagerFactory}. - * - * @return - */ - @Bean public PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() { - return new PersistenceAnnotationBeanPostProcessor(); - } - - /** - * {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as {@link - * org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s. - * - * @return - */ - @Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() { - return new AnnotatedHandlerBeanPostProcessor(); - } - - /** - * Use the pre-defined {@link JpaRepositoryExporter} defined by the user or create a default one. - * - * @return - */ - @Bean public JpaRepositoryExporter jpaRepositoryExporter() { - if(null == customJpaRepositoryExporter) { - return new JpaRepositoryExporter(); - } - - return customJpaRepositoryExporter; - } - - /** - * Use the pre-defined {@link ValidatingRepositoryEventListener} defined by the user or create a default one. - * - * @return - */ - @Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() { - return (null == validatingRepositoryEventListener - ? new ValidatingRepositoryEventListener() - : validatingRepositoryEventListener); - } - - /** - * A special Jackson {@link org.codehaus.jackson.map.Module} implementation that configures converters for entities. - * - * @return - */ - @Bean public RepositoryAwareJacksonModule jacksonModule() { - return new RepositoryAwareJacksonModule(); - } - - /** - * Special Repository-aware {@link org.springframework.http.converter.HttpMessageConverter} that can deal with - * entities and links. - * - * @return - */ - @Bean public RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter() { - return new RepositoryAwareMappingHttpMessageConverter(); - } - - /** - * A {@link org.springframework.data.rest.core.UriResolver} implementation that takes a {@link java.net.URI} and - * turns - * it - * into a top-level domain object. - * - * @return - */ - @Bean public UriToDomainObjectUriResolver domainObjectResolver() { - return new UriToDomainObjectUriResolver(); - } - - /** - * The main REST exporter Spring MVC controller. - * - * @return - * - * @throws Exception - */ - @Bean public RepositoryRestController repositoryRestController() throws Exception { - return new RepositoryRestController(); - } - - @Bean public JsonSchemaController jsonSchemaController() { - return new JsonSchemaController(); - } - - @Bean public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() { - return new BaseUriMethodArgumentResolver(); - } - - @Bean public PagingAndSortingMethodArgumentResolver pagingAndSortingMethodArgumentResolver() { - return new PagingAndSortingMethodArgumentResolver(); - } - - @Bean public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() { - return new ServerHttpRequestMethodArgumentResolver(); - } - - /** - * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the - * {@link RepositoryRestController} class. - * - * @return - */ - @Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() { - return new RepositoryRestHandlerAdapter(); - } - - /** - * Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in the - * {@link RepositoryRestController} class. - * - * @return - */ - @Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() { - return new RepositoryRestHandlerMapping(); - } - - /** - * Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}. - * - * @return - */ - @Bean public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { - ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver(); - er.setCustomArgumentResolvers( - Arrays.asList(new ServerHttpRequestMethodArgumentResolver()) - ); - return er; - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java new file mode 100644 index 000000000..00943fc6d --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java @@ -0,0 +1,150 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.data.rest.core.util.UriUtils.*; +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.net.URI; +import java.util.Enumeration; +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.domain.Page; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.webmvc.support.PagingAndSorting; +import org.springframework.hateoas.Link; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +class RepositoryRestRequest { + + private final RepositoryRestConfiguration config; + private final HttpServletRequest request; + private final PagingAndSorting pagingAndSorting; + private final URI baseUri; + private final RepositoryInformation repoInfo; + private final ResourceMapping repoMapping; + private final Link repoLink; + private final Object repository; + private final RepositoryMethodInvoker repoMethodInvoker; + private final PersistentEntity persistentEntity; + private final ResourceMapping entityMapping; + + public RepositoryRestRequest(RepositoryRestConfiguration config, + Repositories repositories, + HttpServletRequest request, + PagingAndSorting pagingAndSorting, + URI baseUri, + RepositoryInformation repoInfo) { + this.config = config; + this.request = request; + this.pagingAndSorting = pagingAndSorting; + this.baseUri = baseUri; + this.repoInfo = repoInfo; + this.repoMapping = getResourceMapping(config, repoInfo); + if(null == repoMapping) { + this.repoLink = null; + this.repository = null; + this.repoMethodInvoker = null; + this.persistentEntity = null; + this.entityMapping = null; + } else { + this.repoLink = new Link(buildUri(baseUri, repoMapping.getPath()).toString(), repoMapping.getRel()); + this.repository = repositories.getRepositoryFor(repoInfo.getDomainType()); + this.persistentEntity = repositories.getPersistentEntity(repoInfo.getDomainType()); + this.repoMethodInvoker = new RepositoryMethodInvoker(repository, repoInfo, persistentEntity); + this.entityMapping = getResourceMapping(config, persistentEntity); + } + } + + HttpServletRequest getRequest() { + return request; + } + + PagingAndSorting getPagingAndSorting() { + return pagingAndSorting; + } + + URI getBaseUri() { + return baseUri; + } + + RepositoryInformation getRepositoryInformation() { + return repoInfo; + } + + ResourceMapping getRepositoryResourceMapping() { + return repoMapping; + } + + Link getRepositoryLink() { + return repoLink; + } + + Object getRepository() { + return repository; + } + + RepositoryMethodInvoker getRepositoryMethodInvoker() { + return repoMethodInvoker; + } + + PersistentEntity getPersistentEntity() { + return persistentEntity; + } + + ResourceMapping getPersistentEntityResourceMapping() { + return entityMapping; + } + + void addNextLink(Page page, List links) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri); + // Add existing query parameters + addQueryParameters(request, builder); + + builder.queryParam(config.getPageParamName(), page.getNumber() + 1) + .queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize()); + + links.add(new Link(builder.build().toString(), "page.next")); + } + + void addPrevLink(Page page, List links) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri); + // Add existing query parameters + addQueryParameters(request, builder); + + builder.queryParam(config.getPageParamName(), page.getNumber() - 1) + .queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize()); + + links.add(new Link(builder.build().toString(), "page.previous")); + } + + @SuppressWarnings({"unchecked"}) Link buildEntitySelfLink(Object o, ConversionService conversionService) { + BeanWrapper bean = BeanWrapper.create(o, conversionService); + Object id = bean.getProperty(persistentEntity.getIdProperty()); + URI uri = buildUri(baseUri, repoMapping.getPath(), id.toString()); + return new Link(uri.toString(), "self"); + } + + private void addQueryParameters(HttpServletRequest request, + UriComponentsBuilder builder) { + for(Enumeration names = request.getParameterNames(); names.hasMoreElements(); ) { + String name = names.nextElement(); + String value = request.getParameter(name); + if(name.equals(config.getPageParamName()) || name.equals(config.getLimitParamName())) { + continue; + } + + builder.queryParam(name, value); + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..cb6a32bbb --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java @@ -0,0 +1,63 @@ +package org.springframework.data.rest.webmvc; + +import java.net.URI; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.support.PagingAndSorting; +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 RepositoryRestRequestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Autowired + private RepositoryRestConfiguration config; + @Autowired + private Repositories repositories; + @Autowired + private RepositoryInformationHandlerMethodArgumentResolver repoInfoResolver; + @Autowired + private PagingAndSortingMethodArgumentResolver pagingAndSortingResolver; + @Autowired + private BaseUriMethodArgumentResolver baseUriResolver; + + @Override public boolean supportsParameter(MethodParameter parameter) { + return RepositoryRestRequest.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + PagingAndSorting pagingAndSorting = (PagingAndSorting)pagingAndSortingResolver.resolveArgument(parameter, + mavContainer, + webRequest, + binderFactory); + RepositoryInformation repoInfo = (RepositoryInformation)repoInfoResolver.resolveArgument(parameter, + mavContainer, + webRequest, + binderFactory); + URI baseUri = (URI)baseUriResolver.resolveArgument(parameter, + mavContainer, + webRequest, + binderFactory); + + return new RepositoryRestRequest(config, + repositories, + webRequest.getNativeRequest(HttpServletRequest.class), + pagingAndSorting, + baseUri, + repoInfo); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java new file mode 100644 index 000000000..ddde8ce23 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -0,0 +1,241 @@ +package org.springframework.data.rest.webmvc; + +import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.config.ResourceMapping; +import org.springframework.data.rest.repository.BaseUriAwareResource; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.data.rest.repository.invoke.RepositoryMethod; +import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Jon Brisbin + */ +@Controller +@RequestMapping("/{repository}/search") +public class RepositorySearchController extends AbstractRepositoryRestController { + + public RepositorySearchController(Repositories repositories, + RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, + ConversionService conversionService) { + super(repositories, config, domainClassConverter, conversionService); + } + + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-compact+json" + } + ) + @ResponseBody + public Resource list(RepositoryRestRequest repoRequest) { + List links = new ArrayList(); + links.addAll(queryMethodLinks(repoRequest.getBaseUri(), + repoRequest.getPersistentEntity().getType())); + return new Resource(Collections.emptyList(), links); + } + + @RequestMapping( + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse jsonpList(RepositoryRestRequest repoRequest) { + return jsonpWrapResponse(repoRequest, list(repoRequest), HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + @RequestMapping( + value = "/{method}", + method = RequestMethod.GET, + produces = { + "application/json", + "application/x-spring-data-verbose+json" + } + ) + @ResponseBody + public Resource query(RepositoryRestRequest repoRequest, + @PathVariable String method) + throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if(repoMethodInvoker.getQueryMethods().isEmpty()) { + throw new ResourceNotFoundException(); + } + + ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); + String methodName = repoMapping.getNameForPath(method); + RepositoryMethod repoMethod = repoMethodInvoker.getQueryMethods().get(methodName); + if(null == repoMethod) { + for(RepositoryMethod queryMethod : repoMethodInvoker.getQueryMethods().values()) { + String path = findPath(queryMethod.getMethod()); + if(path.equals(method)) { + repoMethod = queryMethod; + break; + } + } + if(null == repoMethod) { + throw new ResourceNotFoundException(); + } + } + + List methodParams = repoMethod.getParameters(); + Object[] paramValues = new Object[methodParams.size()]; + if(!methodParams.isEmpty()) { + for(int i = 0; i < paramValues.length; i++) { + MethodParameter param = methodParams.get(i); + if(Pageable.class.isAssignableFrom(param.getParameterType())) { + paramValues[i] = new PageRequest(repoRequest.getPagingAndSorting().getPageNumber(), + repoRequest.getPagingAndSorting().getPageSize(), + repoRequest.getPagingAndSorting().getSort()); + } else if(Sort.class.isAssignableFrom(param.getParameterType())) { + paramValues[i] = repoRequest.getPagingAndSorting().getSort(); + } else { + String paramName = repoMethod.getParameterNames().get(i); + String[] queryParamVals = repoRequest.getRequest().getParameterValues(paramName); + if(null == queryParamVals) { + if(paramName.startsWith("arg")) { + throw new IllegalArgumentException("No @Param annotation found on query method " + + repoMethod.getMethod().getName() + + " for parameter " + param.getParameterName()); + } else { + throw new IllegalArgumentException("No query parameter specified for " + + repoMethod.getMethod().getName() + " param '" + + paramName + "'"); + } + } + paramValues[i] = methodParameterConversionService.convert(queryParamVals, param); + } + } + } + + BaseUriAwareResource resources; + List links = new ArrayList(); + Object result = repoMethodInvoker.invokeQueryMethod(repoMethod, paramValues); + if(result instanceof Page) { + Page page = (Page)result; + if(page.hasPreviousPage()) { + repoRequest.addPrevLink(page, links); + } + if(page.hasNextPage()) { + repoRequest.addNextLink(page, links); + } + if(page.hasContent()) { + resources = entitiesToResource(repoRequest, page.getContent()); + } else { + resources = new BaseUriAwareResource(EMPTY_RESOURCE_LIST); + } + } else if(result instanceof Iterable) { + resources = entitiesToResource(repoRequest, (Iterable)result); + } else if(null == result) { + resources = new BaseUriAwareResource(EMPTY_RESOURCE_LIST); + } else { + PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(), + result, + repoRequest.getBaseUri()); + per.add(repoRequest.buildEntitySelfLink(result, conversionService)); + resources = per; + } + resources.setBaseUri(repoRequest.getBaseUri()) + .add(links); + + return resources; + } + + @RequestMapping( + value = "/{method}", + method = RequestMethod.GET, + produces = { + "application/x-spring-data-compact+json" + } + ) + @ResponseBody + public Resource queryCompact(RepositoryRestRequest repoRequest, + @PathVariable String method) + throws ResourceNotFoundException { + List links = new ArrayList(); + + Resource resource = query(repoRequest, method); + links.addAll(resource.getLinks()); + + if(resource.getContent() instanceof Iterable) { + Iterable iter = (Iterable)resource.getContent(); + for(Object obj : iter) { + if(null != obj && obj instanceof Resource) { + Resource res = (Resource)obj; + links.add(resourceLink(repoRequest, res)); + } + } + } else if(resource.getContent() instanceof Resource) { + Resource res = (Resource)resource.getContent(); + links.add(resourceLink(repoRequest, res)); + } + + return new Resource(EMPTY_RESOURCE_LIST, links); + } + + @RequestMapping( + value = "/{method}", + method = RequestMethod.GET, + produces = { + "application/javascript" + } + ) + @ResponseBody + public JsonpResponse> jsonpQuery(RepositoryRestRequest repoRequest, + @PathVariable String method) + throws ResourceNotFoundException { + return jsonpWrapResponse(repoRequest, query(repoRequest, method), HttpStatus.OK); + } + + @SuppressWarnings({"unchecked"}) + private BaseUriAwareResource entitiesToResource(RepositoryRestRequest repoRequest, Iterable entities) { + List> resources = new ArrayList>(); + for(Object obj : entities) { + if(null == obj) { + resources.add(null); + break; + } + + PersistentEntity persistentEntity = repositories.getPersistentEntity(obj.getClass()); + if(null == persistentEntity) { + resources.add(new BaseUriAwareResource(obj) + .setBaseUri(repoRequest.getBaseUri())); + continue; + } + + PersistentEntityResource per = PersistentEntityResource.wrap(persistentEntity, obj, repoRequest.getBaseUri()); + per.add(repoRequest.buildEntitySelfLink(obj, conversionService)); + resources.add(per); + } + return new BaseUriAwareResource(resources) + .setBaseUri(repoRequest.getBaseUri()); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceNotFoundException.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceNotFoundException.java new file mode 100644 index 000000000..42d92cdd1 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceNotFoundException.java @@ -0,0 +1,20 @@ +package org.springframework.data.rest.webmvc; + +/** + * Indicates a resource was not found. + * + * @author Jon Brisbin + */ +public class ResourceNotFoundException extends Exception { + public ResourceNotFoundException() { + super("Resource not found"); + } + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java deleted file mode 100644 index 7b9b18140..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.URI; -import java.util.HashSet; -import java.util.Set; - -import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.Assert; - -/** - * A special {@link org.springframework.http.converter.HttpMessageConverter} that can take various input formats and - * produce a plain-text list of URIs (or read the same). - * - * @author Jon Brisbin - */ -public class UriListHttpMessageConverter extends AbstractHttpMessageConverter { - - public UriListHttpMessageConverter() { - super(MediaTypes.URI_LIST); - } - - @Override protected boolean supports(Class clazz) { - return (RepositoryMethodResponse.class.isAssignableFrom(clazz) - || Resource.class.isAssignableFrom(clazz) - || Set.class.isAssignableFrom(clazz)); - } - - @SuppressWarnings({"unchecked"}) - @Override - protected Object readInternal(Class clazz, - HttpInputMessage inputMessage) throws IOException, - HttpMessageNotReadableException { - Assert.isTrue((Resource.class.isAssignableFrom(clazz) || Set.class.isAssignableFrom(clazz)), - "Cannot read a text/uri-list into a " + clazz); - - String rel = inputMessage.getHeaders().getFirst("x-spring-data-urilist-rel"); - if(null == rel && inputMessage instanceof ServletServerHttpRequest) { - rel = ((ServletServerHttpRequest)inputMessage).getURI().getPath().substring(1).replaceAll("/", "."); - } - BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody())); - - Set links = new HashSet(); - String line; - while(null != (line = reader.readLine())) { - links.add(new Link(URI.create(line.trim()).toString(), rel)); - } - - return (Set.class.isAssignableFrom(clazz) ? links : new Resource("", links)); - } - - @Override - protected void writeInternal(Object links, HttpOutputMessage outputMessage) - throws IOException, - HttpMessageNotWritableException { - OutputStream body = outputMessage.getBody(); - if(links instanceof Set) { - for(Object o : (Set)links) { - if(o instanceof Link) { - body.write(((Link)o).getHref().getBytes()); - } else { - body.write(o.toString().getBytes()); - } - body.write('\n'); - } - } else if(links instanceof RepositoryMethodResponse) { - writeInternal(((RepositoryMethodResponse)links).getLinks(), outputMessage); - } else if(links instanceof Resource) { - writeInternal(((Resource)links).getLinks(), outputMessage); - } - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/annotation/BaseURI.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/annotation/BaseURI.java new file mode 100644 index 000000000..b4b92c0a1 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/annotation/BaseURI.java @@ -0,0 +1,16 @@ +package org.springframework.data.rest.webmvc.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation to denote which {@link java.net.URI} parameter should be resolved to the request base URI. + * + * @author Jon Brisbin + */ +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface BaseURI { +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java new file mode 100644 index 000000000..d2848d3b3 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -0,0 +1,462 @@ +package org.springframework.data.rest.webmvc.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.data.repository.support.DomainClassConverter; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.convert.ISO8601DateConverter; +import org.springframework.data.rest.convert.UUIDConverter; +import org.springframework.data.rest.repository.UriDomainClassConverter; +import org.springframework.data.rest.repository.context.AnnotatedHandlerBeanPostProcessor; +import org.springframework.data.rest.repository.context.RepositoriesFactoryBean; +import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener; +import org.springframework.data.rest.repository.json.PersistentEntityJackson2Module; +import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.rest.repository.support.DomainObjectMerger; +import org.springframework.data.rest.webmvc.BaseUriMethodArgumentResolver; +import org.springframework.data.rest.webmvc.PagingAndSortingMethodArgumentResolver; +import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.RepositoryController; +import org.springframework.data.rest.webmvc.RepositoryEntityController; +import org.springframework.data.rest.webmvc.RepositoryEntityLinksMethodArgumentResolver; +import org.springframework.data.rest.webmvc.RepositoryInformationHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.RepositoryPropertyReferenceController; +import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; +import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping; +import org.springframework.data.rest.webmvc.RepositoryRestRequestHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.RepositorySearchController; +import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; +import org.springframework.data.rest.webmvc.convert.JsonpResponseHttpMessageConverter; +import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; +import org.springframework.util.ClassUtils; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; + +/** + * Main application configuration for Spring Data REST. To customize how the exporter works, subclass this and override + * any of the {@literal configure*} methods. + *

+ * Any XML files located in the classpath under the {@literal META-INF/spring-data-rest/} path will be automatically + * found and loaded into this {@link org.springframework.context.ApplicationContext}. + * + * @author Jon Brisbin + */ +@Configuration +@ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml") +public class RepositoryRestMvcConfiguration { + + private static final boolean IS_HIBERNATE4_MODULE_AVAILABLE = ClassUtils.isPresent( + "com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module", + RepositoryRestMvcConfiguration.class.getClassLoader() + ); + private static final boolean IS_JODA_MODULE_AVAILABLE = ClassUtils.isPresent( + "com.fasterxml.jackson.datatype.joda.JodaModule", + RepositoryRestMvcConfiguration.class.getClassLoader() + ); + + @Bean public RepositoriesFactoryBean repositories() { + return new RepositoriesFactoryBean(); + } + + @Bean public DefaultFormattingConversionService defaultConversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(UUIDConverter.INSTANCE); + conversionService.addConverter(ISO8601DateConverter.INSTANCE); + configureConversionService(conversionService); + return conversionService; + } + + @Bean public DomainClassConverter domainClassConverter() { + return new DomainClassConverter(defaultConversionService()); + } + + @Bean public UriDomainClassConverter uriDomainClassConverter() { + return new UriDomainClassConverter(); + } + + /** + * {@link org.springframework.context.ApplicationListener} implementation for invoking {@link + * org.springframework.validation.Validator} instances assigned to specific domain types. + */ + @Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() { + ValidatingRepositoryEventListener listener = new ValidatingRepositoryEventListener(); + configureValidatingRepositoryEventListener(listener); + return listener; + } + + /** + * Main configuration for the REST exporter. + */ + @Bean public RepositoryRestConfiguration config() { + RepositoryRestConfiguration config = new RepositoryRestConfiguration(); + configureRepositoryRestConfiguration(config); + return config; + } + + /** + * For getting access to the {@link javax.persistence.EntityManagerFactory}. + * + * @return + */ + @Bean public PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor() { + return new PersistenceAnnotationBeanPostProcessor(); + } + + /** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as {@link + * org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s. + * + * @return + */ + @Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() { + return new AnnotatedHandlerBeanPostProcessor(); + } + + /** + * For merging incoming objects materialized from JSON with existing domain objects loaded from the repository. + * + * @return + * + * @throws Exception + */ + @Bean public DomainObjectMerger domainObjectMerger() throws Exception { + return new DomainObjectMerger( + repositories().getObject(), + defaultConversionService() + ); + } + + /** + * The controller that handles top-level requests for listing what repositories are available. + * + * @return + * + * @throws Exception + */ + @Bean public RepositoryController repositoryController() throws Exception { + return new RepositoryController( + repositories().getObject(), + config(), + domainClassConverter(), + defaultConversionService() + ); + } + + /** + * The controller responsible for handling requests to display or those that modify an entity. + * + * @return + * + * @throws Exception + */ + @Bean public RepositoryEntityController repositoryEntityController() throws Exception { + return new RepositoryEntityController( + repositories().getObject(), + config(), + domainClassConverter(), + defaultConversionService() + ); + } + + /** + * The controller responsible for managing links of property references. + * + * @return + * + * @throws Exception + */ + @Bean public RepositoryPropertyReferenceController propertyReferenceController() throws Exception { + return new RepositoryPropertyReferenceController( + repositories().getObject(), + config(), + domainClassConverter(), + defaultConversionService() + ); + } + + /** + * The controller responsible for performing searches. + * + * @return + * + * @throws Exception + */ + @Bean public RepositorySearchController repositorySearchController() throws Exception { + return new RepositorySearchController( + repositories().getObject(), + config(), + domainClassConverter(), + defaultConversionService() + ); + } + + /** + * Resolves the base {@link java.net.URI} under which this application is configured. + * + * @return + */ + @Bean public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() { + return new BaseUriMethodArgumentResolver(); + } + + /** + * Resolves the paging and sorting information from the query parameters based on the current configuration settings. + * + * @return + */ + @Bean public PagingAndSortingMethodArgumentResolver pagingAndSortingMethodArgumentResolver() { + return new PagingAndSortingMethodArgumentResolver(); + } + + /** + * Turns an {@link javax.servlet.http.HttpServletRequest} into a {@link org.springframework.http.server.ServerHttpRequest}. + * + * @return + */ + @Bean public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() { + return new ServerHttpRequestMethodArgumentResolver(); + } + + /** + * Resolves the {@link org.springframework.data.repository.core.RepositoryInformation} for this request. + * + * @return + */ + @Bean public RepositoryInformationHandlerMethodArgumentResolver repoInfoMethodArgumentResolver() { + return new RepositoryInformationHandlerMethodArgumentResolver(); + } + + /** + * A convenience resolver that pulls together all the information needed to service a request. + * + * @return + */ + @Bean public RepositoryRestRequestHandlerMethodArgumentResolver repoRequestArgumentResolver() { + return new RepositoryRestRequestHandlerMethodArgumentResolver(); + } + + @Bean public RepositoryEntityLinksMethodArgumentResolver entityLinksMethodArgumentResolver() { + return new RepositoryEntityLinksMethodArgumentResolver(); + } + + /** + * Reads incoming JSON into an entity. + * + * @return + */ + @Bean public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() { + List> messageConverters = defaultMessageConverters(); + configureHttpMessageConverters(messageConverters); + + return new PersistentEntityResourceHandlerMethodArgumentResolver(messageConverters); + } + + /** + * Turns a domain class into a {@link org.springframework.data.rest.repository.json.JsonSchema}. + * + * @return + */ + @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { + return new PersistentEntityToJsonSchemaConverter(); + } + + /** + * The Jackson {@link ObjectMapper} used internally. + * + * @return + */ + @Bean public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); + // Our special PersistentEntityResource Module + objectMapper.registerModule(persistentEntityJackson2Module()); + // Hibernate types + if(IS_HIBERNATE4_MODULE_AVAILABLE) { + objectMapper.registerModule(new com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module()); + } + // JODA time + if(IS_JODA_MODULE_AVAILABLE) { + objectMapper.registerModule(new com.fasterxml.jackson.datatype.joda.JodaModule()); + } + // Configure custom Modules + configureJacksonObjectMapper(objectMapper); + + return objectMapper; + } + + /** + * The {@link HttpMessageConverter} used by Spring MVC to read and write JSON data. + * + * @return + */ + @Bean public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() { + MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); + jacksonConverter.setObjectMapper(objectMapper()); + jacksonConverter.setSupportedMediaTypes(Arrays.asList( + MediaType.APPLICATION_JSON, + MediaType.valueOf("application/schema+json"), + MediaType.valueOf("application/x-spring-data-verbose+json"), + MediaType.valueOf("application/x-spring-data-compact+json") + )); + return jacksonConverter; + } + + /** + * The {@link HttpMessageConverter} used to create JSONP responses. + * + * @return + */ + @Bean public JsonpResponseHttpMessageConverter jsonpHttpMessageConverter() { + return new JsonpResponseHttpMessageConverter(jacksonHttpMessageConverter()); + } + + /** + * The {@link HttpMessageConverter} used to create {@literal text/uri-list} responses. + * + * @return + */ + @Bean public UriListHttpMessageConverter uriListHttpMessageConverter() { + return new UriListHttpMessageConverter(); + } + + /** + * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in + * the provided controller classes. + * + * @return + */ + @Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() { + List> messageConverters = defaultMessageConverters(); + configureHttpMessageConverters(messageConverters); + + RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(); + handlerAdapter.setMessageConverters(messageConverters); + handlerAdapter.setCustomArgumentResolvers(defaultMethodArgumentResolvers()); + + return handlerAdapter; + } + + /** + * Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in + * the provided controller classes. + * + * @return + */ + @Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() { + return new RepositoryRestHandlerMapping(); + } + + /** + * Jackson module responsible for intelligently serializing and deserializing JSON that corresponds to an entity. + * + * @return + */ + @Bean public PersistentEntityJackson2Module persistentEntityJackson2Module() { + return new PersistentEntityJackson2Module(defaultConversionService()); + } + + /** + * Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}. + * + * @return + */ + @Bean public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { + ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver(); + er.setCustomArgumentResolvers(defaultMethodArgumentResolvers()); + + List> messageConverters = defaultMessageConverters(); + configureHttpMessageConverters(messageConverters); + + er.setMessageConverters(messageConverters); + configureExceptionHandlerExceptionResolver(er); + + return er; + } + + private List> defaultMessageConverters() { + List> messageConverters = new ArrayList>(); + messageConverters.add(jacksonHttpMessageConverter()); + messageConverters.add(jsonpHttpMessageConverter()); + messageConverters.add(uriListHttpMessageConverter()); + return messageConverters; + } + + private List defaultMethodArgumentResolvers() { + return Arrays.asList(baseUriMethodArgumentResolver(), + pagingAndSortingMethodArgumentResolver(), + serverHttpRequestMethodArgumentResolver(), + repoInfoMethodArgumentResolver(), + repoRequestArgumentResolver(), + persistentEntityArgumentResolver(), + entityLinksMethodArgumentResolver()); + } + + /** + * Override this method to add additional configuration. + * + * @param config + * Main configuration bean. + */ + protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { + } + + /** + * Override this method to add your own converters. + * + * @param conversionService + * Default ConversionService bean. + */ + protected void configureConversionService(ConfigurableConversionService conversionService) { + } + + /** + * Override this method to add validators manually. + * + * @param validatingListener + * The {@link org.springframework.context.ApplicationListener} responsible for invoking {@link + * org.springframework.validation.Validator} instances. + */ + protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) { + } + + /** + * Configure the {@link ExceptionHandlerExceptionResolver}. + * + * @param exceptionResolver + * The default exception resolver on which you can add custom argument resolvers. + */ + protected void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) { + } + + /** + * Configure the available {@link HttpMessageConverter}s by adding your own. + * + * @param messageConverters + * The converters to be used by the system. + */ + protected void configureHttpMessageConverters(List> messageConverters) { + } + + /** + * Configure the Jackson {@link ObjectMapper} directly. + * + * @param objectMapper + * The {@literal ObjectMapper} to be used by the system. + */ + protected void configureJacksonObjectMapper(ObjectMapper objectMapper) { + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/JsonpResponseHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/JsonpResponseHttpMessageConverter.java new file mode 100644 index 000000000..48aa16856 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/JsonpResponseHttpMessageConverter.java @@ -0,0 +1,84 @@ +package org.springframework.data.rest.webmvc.convert; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.rest.webmvc.support.JsonpResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + +/** + * @author Jon Brisbin + */ +public class JsonpResponseHttpMessageConverter implements HttpMessageConverter> { + + private static final MediaType APPLICATION_JAVASCRIPT = MediaType.valueOf("application/javascript"); + private static final List SUPPORTED_TYPES = Arrays.asList( + APPLICATION_JAVASCRIPT + ); + + private final MappingJackson2HttpMessageConverter jacksonConverter; + + public JsonpResponseHttpMessageConverter(MappingJackson2HttpMessageConverter jacksonConverter) { + this.jacksonConverter = jacksonConverter; + } + + @Override public boolean canRead(Class clazz, MediaType mediaType) { + return false; + } + + @Override public boolean canWrite(Class clazz, MediaType mediaType) { + return JsonpResponse.class.isAssignableFrom(clazz) && mediaType.getSubtype().contains("javascript"); + } + + @Override public List getSupportedMediaTypes() { + return SUPPORTED_TYPES; + } + + @Override + public JsonpResponse read(Class> clazz, + HttpInputMessage inputMessage) throws IOException, + HttpMessageNotReadableException { + throw new HttpMessageNotReadableException("JSONP messages are not readable."); + } + + @Override + public void write(JsonpResponse jsonpResponse, + MediaType contentType, + final HttpOutputMessage outputMessage) throws IOException, + HttpMessageNotWritableException { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write((jsonpResponse.getCallbackParam() + "(").getBytes()); + + jacksonConverter.write(jsonpResponse.getResponseEntity().getBody(), + MediaType.APPLICATION_JSON, + new HttpOutputMessage() { + @Override public OutputStream getBody() throws IOException { + return bytes; + } + + @Override public HttpHeaders getHeaders() { + return outputMessage.getHeaders(); + } + }); + + bytes.write(");".getBytes()); + + byte[] byteArray = bytes.toByteArray(); + + outputMessage.getHeaders().setContentType(APPLICATION_JAVASCRIPT); + outputMessage.getHeaders().setContentLength(byteArray.length); + outputMessage.getBody().flush(); + outputMessage.getBody().write(byteArray); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java new file mode 100644 index 000000000..075d6a0c5 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java @@ -0,0 +1,73 @@ +package org.springframework.data.rest.webmvc.convert; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +/** + * @author Jon Brisbin + */ +public class UriListHttpMessageConverter implements HttpMessageConverter> { + + private static final List MEDIA_TYPES = new ArrayList(); + + static { + MEDIA_TYPES.add(MediaType.parseMediaType("text/uri-list")); + } + + @Override public boolean canRead(Class clazz, MediaType mediaType) { + if(null == mediaType) { + return false; + } + return Resource.class.isAssignableFrom(clazz) && mediaType.getSubtype().contains("uri-list"); + } + + @Override public boolean canWrite(Class clazz, MediaType mediaType) { + return canRead(clazz, mediaType); + } + + @Override public List getSupportedMediaTypes() { + return MEDIA_TYPES; + } + + @Override public Resource read(Class> clazz, + HttpInputMessage inputMessage) + throws IOException, + HttpMessageNotReadableException { + List links = new ArrayList(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody())); + String line; + while(null != (line = reader.readLine())) { + links.add(new Link(line)); + } + return new Resource(Collections.emptyList(), links); + } + + @Override public void write(Resource resource, + MediaType contentType, + HttpOutputMessage outputMessage) + throws IOException, + HttpMessageNotWritableException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputMessage.getBody())); + for(Link link : resource.getLinks()) { + writer.write(link.getHref()); + writer.newLine(); + } + writer.flush(); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonUtil.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonUtil.java deleted file mode 100644 index bf17ea5af..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.springframework.data.rest.webmvc.json; - -import java.io.IOException; -import java.util.Arrays; - -import org.codehaus.jackson.JsonEncoding; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.map.ObjectMapper; -import org.springframework.data.rest.webmvc.MediaTypes; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; - -/** - * Utility class for creating a custom-configured {@see MappingJacksonHttpMessageConverter} that has our own - * serializers and {@see MediaType} mappings on it. - * - * @author Jon Brisbin - */ -public abstract class JacksonUtil { - - private JacksonUtil() { - } - - public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter(final ObjectMapper objectMapper) { - // We want to support all our custom types of JSON and also the catch-all - MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() { - { - setSupportedMediaTypes(Arrays.asList( - MediaType.APPLICATION_JSON, - MediaTypes.COMPACT_JSON, - MediaTypes.VERBOSE_JSON - )); - } - - @Override public boolean canRead(Class clazz, MediaType mediaType) { - if(!canRead(mediaType)) { - return false; - } - return true; - } - - @Override public boolean canWrite(Class clazz, MediaType mediaType) { - if(!canWrite(mediaType)) { - return false; - } - return true; - } - - @Override - protected void writeInternal(Object object, - HttpOutputMessage outputMessage) throws IOException, - HttpMessageNotWritableException { - JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); - // Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration - JsonGenerator jsonGenerator = objectMapper - .getJsonFactory() - .createJsonGenerator(outputMessage.getBody(), encoding) - .useDefaultPrettyPrinter(); - try { - objectMapper.writeValue(jsonGenerator, object); - } catch(IOException ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - } - }; - jsonConverter.setObjectMapper(objectMapper); - - return jsonConverter; - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchemaController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchemaController.java deleted file mode 100644 index 0ead75f0c..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchemaController.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.springframework.data.rest.webmvc.json; - -import java.io.IOException; -import java.net.URI; - -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.SerializationConfig; -import org.codehaus.jackson.schema.JsonSchema; -import org.springframework.data.rest.repository.RepositoryExporterSupport; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * A controller to output JSON schema based on Jackson's schema generator. - * - * @author Jon Brisbin - */ -public class JsonSchemaController extends RepositoryExporterSupport { - - private ObjectMapper mapper = new ObjectMapper(); - - { - mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true); - } - - @RequestMapping( - value = "/{repository}/schema", - method = RequestMethod.GET, - produces = "application/json" - ) - @ResponseBody - public ResponseEntity schemaForRepository(URI baseUri, - @PathVariable String repository) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - if(null == repoMeta) { - return new ResponseEntity(HttpStatus.NOT_FOUND); - } - - JsonSchema schema = mapper.generateJsonSchema(repoMeta.domainType()); - - URI schemaUri = UriComponentsBuilder.fromUri(baseUri) - .pathSegment(repository, "schema") - .build() - .toUri(); - URI requestUri = UriComponentsBuilder.fromUri(baseUri) - .pathSegment(repository) - .build() - .toUri(); - Resource resource = new Resource(schema, - new Link(schemaUri.toString(), "self"), - new Link(requestUri.toString(), repoMeta.rel())); - - String output = mapper.writeValueAsString(resource); - - return new ResponseEntity(output, HttpStatus.OK); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/RepositoryAwareJacksonModule.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/RepositoryAwareJacksonModule.java deleted file mode 100644 index 79c88c0d6..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/RepositoryAwareJacksonModule.java +++ /dev/null @@ -1,385 +0,0 @@ -package org.springframework.data.rest.webmvc.json; - -import static org.springframework.data.rest.core.util.UriUtils.*; -import static org.springframework.data.util.ClassTypeInformation.*; - -import java.io.IOException; -import java.io.Serializable; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.Version; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.KeyDeserializer; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.deser.std.StdDeserializer; -import org.codehaus.jackson.map.module.SimpleDeserializers; -import org.codehaus.jackson.map.module.SimpleKeyDeserializers; -import org.codehaus.jackson.map.module.SimpleModule; -import org.codehaus.jackson.map.module.SimpleSerializers; -import org.codehaus.jackson.map.ser.std.SerializerBase; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.rest.repository.AttributeMetadata; -import org.springframework.data.rest.repository.RepositoryExporter; -import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.data.rest.repository.UriToDomainObjectUriResolver; -import org.springframework.data.rest.webmvc.EntityToResourceConverter; -import org.springframework.data.rest.webmvc.RepositoryRestConfiguration; -import org.springframework.data.util.TypeInformation; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceProcessor; -import org.springframework.http.converter.HttpMessageNotReadableException; - -/** - * Special implementation of a Jackson {@link org.codehaus.jackson.map.Module} to handle properly serializing and - * deserializing entities with links. - * - * @author Jon Brisbin - */ -public class RepositoryAwareJacksonModule extends SimpleModule implements InitializingBean { - - @Autowired(required = false) - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; - @Autowired(required = false) - protected List repositoryExporters = Collections.emptyList(); - @Autowired(required = false) - private List conversionServices = Collections.emptyList(); - @Autowired(required = false) - private List>> resourceProcessors = Collections.emptyList(); - private Multimap, ResourceProcessor>> resourceProcessorMap = ArrayListMultimap.create(); - @Autowired - private UriToDomainObjectUriResolver domainObjectResolver; - private final GenericConversionService conversionService = new GenericConversionService(); - - private final SimpleSerializers sers = new SimpleSerializers(); - private final SimpleDeserializers dsers = new SimpleDeserializers(); - private final SimpleSerializers keySers = new SimpleSerializers(); - private final SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers(); - - public RepositoryAwareJacksonModule() { - super("RepositoryAwareJacksonModule", Version.unknownVersion()); - } - - @SuppressWarnings({"unchecked"}) - @Override public void afterPropertiesSet() throws Exception { - for(RepositoryExporter repoExp : repositoryExporters) { - for(String repoName : new ArrayList(repoExp.repositoryNames())) { - RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName); - Class domainType = repoMeta.entityMetadata().type(); - TypeInformation domainTypeInfo = from(domainType); - - for(ResourceProcessor> rp : resourceProcessors) { - TypeInformation resourceType = from(rp.getClass()) - .getSuperTypeInformation(ResourceProcessor.class) - .getComponentType(); - Class processorType = resourceType.getType(); - TypeInformation componentType = resourceType.getComponentType(); - - if(Resource.class.isAssignableFrom(processorType) && componentType.isAssignableFrom(domainTypeInfo)) { - resourceProcessorMap.put(domainType, rp); - } - } - - conversionService.addConverter(domainType, Resource.class, new EntityToResourceConverter(config, repoMeta)); - - sers.addSerializer(domainType, new DomainObjectToResourceSerializer(domainType)); - keySers.addSerializer(domainType, new DomainObjectToStringKeySerializer(domainType, repoMeta)); - - dsers.addDeserializer(domainType, new LinkToDomainObjectDeserializer(domainType, repoMeta)); - keyDsers.addDeserializer(domainType, new KeyToDomainObjectDeserializer()); - } - } - } - - @Override public void setupModule(SetupContext context) { - context.addSerializers(sers); - context.addKeySerializers(keySers); - context.addDeserializers(dsers); - context.addKeyDeserializers(keyDsers); - } - - private class DomainObjectToResourceSerializer extends SerializerBase { - private DomainObjectToResourceSerializer(Class t) { - super(t); - } - - @SuppressWarnings({"unchecked"}) - @Override public void serialize(Object value, - JsonGenerator jgen, - SerializerProvider provider) throws IOException, - JsonGenerationException { - if(null == value) { - provider.defaultSerializeNull(jgen); - return; - } - - if(!conversionService.canConvert(value.getClass(), Resource.class)) { - provider.defaultSerializeValue(value, jgen); - return; - } - - // Process the resource first to catch user stuff - Resource resource = new Resource(value); - for(ResourceProcessor> rp : resourceProcessorMap.get(value.getClass())) { - resource = rp.process(resource); - } - // Maybe convert the resource so we can extract linked properties - if(null == resource.getContent()) { - provider.defaultSerializeNull(jgen); - return; - } - - Class sourceType = resource.getContent().getClass(); - ConversionService entityConversionSvc = conversionService; - for(ConversionService cs : conversionServices) { - if(cs.canConvert(sourceType, Resource.class)) { - entityConversionSvc = cs; - break; - } - } - if(entityConversionSvc.canConvert(sourceType, Resource.class)) { - List links = resource.getLinks(); - resource = entityConversionSvc.convert(value, Resource.class); - resource.add(links); - } - - jgen.writeObject(resource); - - } - - } - - private class DomainObjectToStringKeySerializer extends SerializerBase { - - private final RepositoryMetadata repoMeta; - private final AttributeMetadata idAttr; - - private DomainObjectToStringKeySerializer(Class t, RepositoryMetadata repoMeta) { - super(t); - this.repoMeta = repoMeta; - if(null != repoMeta) { - idAttr = repoMeta.entityMetadata().idAttribute(); - } else { - idAttr = null; - } - } - - @Override public void serialize(Object value, - JsonGenerator jgen, - SerializerProvider provider) throws IOException, - JsonGenerationException { - if(null == value) { - provider.defaultSerializeNull(jgen); - return; - } - if(null == repoMeta) { - provider.defaultSerializeValue(value, jgen); - return; - } - - Serializable serId = (Serializable)idAttr.get(value); - String sId = null; - for(ConversionService cs : conversionServices) { - if(cs.canConvert(idAttr.type(), String.class)) { - sId = cs.convert(serId, String.class); - break; - } - } - if(null == sId) { - sId = serId.toString(); - } - - URI href = buildUri(config.getBaseUri(), repoMeta.name(), sId); - - jgen.writeString("@" + href.toString()); - } - - } - - private class LinkToDomainObjectDeserializer extends StdDeserializer { - - protected final RepositoryMetadata repoMeta; - - private LinkToDomainObjectDeserializer(Class vc, RepositoryMetadata repoMeta) { - super(vc); - this.repoMeta = repoMeta; - } - - @SuppressWarnings({"unchecked"}) - @Override public Object deserialize(JsonParser jp, - DeserializationContext ctxt) throws IOException, - JsonProcessingException { - Object entity = BeanUtils.instantiateClass(getValueClass()); - for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { - String name = jp.getCurrentName(); - switch(tok) { - case FIELD_NAME: { - // Read the attribute metadata - AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name); - Object val = null; - - if(name.startsWith("@http")) { - entity = domainObjectResolver.resolve( - config.getBaseUri(), - URI.create(name.substring(1)) - ); - continue; - } - - if("href".equals(name)) { - entity = domainObjectResolver.resolve( - config.getBaseUri(), - URI.create(jp.nextTextValue()) - ); - continue; - } - - if("rel".equals(name)) { - // rel is currently ignored - continue; - } - - if("links".equals(name)) { - if((tok = jp.nextToken()) == JsonToken.START_ARRAY) { - while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { - // Advance past the links - } - } else if(tok == JsonToken.VALUE_NULL) { - // skip null value - } else { - throw new HttpMessageNotReadableException( - "Property 'links' is not of array type. Either eliminate this property from the document or make it an array."); - } - continue; - } - - if(null == attrMeta) { - // do nothing - continue; - } - - // Try and read the value of this attribute. - // The method of doing that varies based on the type of the property. - if(attrMeta.isCollectionLike()) { - Collection c = attrMeta.asCollection(entity); - if(null == c || c == Collections.emptyList()) { - c = new ArrayList(); - } - - if((tok = jp.nextToken()) == JsonToken.START_ARRAY) { - while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { - Object cval = jp.readValueAs(attrMeta.elementType()); - c.add(cval); - } - - val = c; - - } else if(tok == JsonToken.VALUE_NULL) { - val = null; - } else { - throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection."); - } - } else if(attrMeta.isSetLike()) { - Set s = attrMeta.asSet(entity); - if(null == s || s == Collections.emptySet()) { - s = new HashSet(); - } - - if((tok = jp.nextToken()) == JsonToken.START_ARRAY) { - while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { - Object sval = jp.readValueAs(attrMeta.elementType()); - s.add(sval); - } - - val = s; - - } else if(tok == JsonToken.VALUE_NULL) { - val = null; - } else { - throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Set."); - } - } else if(attrMeta.isMapLike()) { - Map m = attrMeta.asMap(entity); - if(null == m || m == Collections.emptyMap()) { - m = new HashMap(); - } - - if((tok = jp.nextToken()) == JsonToken.START_OBJECT) { - do { - name = jp.getCurrentName(); - Object mkey = ( - name.startsWith("@http") - ? domainObjectResolver.resolve( - config.getBaseUri(), - URI.create(name.substring(1)) - ) - : name - ); - tok = jp.nextToken(); - Object mval = jp.readValueAs(attrMeta.elementType()); - - m.put(mkey, mval); - } while((tok = jp.nextToken()) != JsonToken.END_OBJECT); - - val = m; - - } else if(tok == JsonToken.VALUE_NULL) { - val = null; - } else { - throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map."); - } - } else { - if((tok = jp.nextToken()) != JsonToken.VALUE_NULL) { - val = jp.readValueAs(attrMeta.type()); - } - } - - if(null != val) { - attrMeta.set(val, entity); - } - - break; - } - } - } - - return entity; - } - - } - - private class KeyToDomainObjectDeserializer extends KeyDeserializer { - @Override public Object deserializeKey(String key, - DeserializationContext ctxt) throws IOException, - JsonProcessingException { - if(key.startsWith("@http")) { - return domainObjectResolver.resolve( - config.getBaseUri(), - URI.create(key.substring(1)) - ); - } else { - return key; - } - } - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BaseUriLinkBuilder.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BaseUriLinkBuilder.java new file mode 100644 index 000000000..6465d0a95 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BaseUriLinkBuilder.java @@ -0,0 +1,29 @@ +package org.springframework.data.rest.webmvc.support; + +import java.net.URI; + +import org.springframework.hateoas.core.LinkBuilderSupport; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +public class BaseUriLinkBuilder extends LinkBuilderSupport { + + public BaseUriLinkBuilder(UriComponentsBuilder builder) { + super(builder); + } + + public static BaseUriLinkBuilder create(URI baseUri) { + return new BaseUriLinkBuilder(UriComponentsBuilder.fromUri(baseUri)); + } + + @Override protected BaseUriLinkBuilder getThis() { + return this; + } + + @Override protected BaseUriLinkBuilder createNewInstance(UriComponentsBuilder builder) { + return new BaseUriLinkBuilder(builder); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationExceptionMessage.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationExceptionMessage.java new file mode 100644 index 000000000..eeb878414 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationExceptionMessage.java @@ -0,0 +1,36 @@ +package org.springframework.data.rest.webmvc.support; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.context.MessageSource; + +/** + * @author Jon Brisbin + */ +public class ConstraintViolationExceptionMessage { + + private final ConstraintViolationException cve; + private final List messages = new ArrayList(); + + public ConstraintViolationExceptionMessage(ConstraintViolationException cve, MessageSource msgSrc) { + this.cve = cve; + for(ConstraintViolation cv : cve.getConstraintViolations()) { + messages.add(new ConstraintViolationMessage(cv, msgSrc)); + } + } + + @JsonProperty("cause") + public String getCause() { + return cve.getMessage(); + } + + @JsonProperty("messages") + public List getMessages() { + return messages; + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationMessage.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationMessage.java new file mode 100644 index 000000000..09ce984e1 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ConstraintViolationMessage.java @@ -0,0 +1,52 @@ +package org.springframework.data.rest.webmvc.support; + +import static java.lang.String.*; + +import javax.validation.ConstraintViolation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.context.MessageSource; + +/** + * A helper class to encapsulate {@link ConstraintViolation} errors. + * + * @author Jon Brisbin + */ +public class ConstraintViolationMessage { + + private final ConstraintViolation violation; + private final String message; + + public ConstraintViolationMessage(ConstraintViolation violation, MessageSource msgSrc) { + this.violation = violation; + this.message = msgSrc.getMessage(violation.getMessageTemplate(), + new Object[]{ + violation.getLeafBean().getClass().getSimpleName(), + violation.getPropertyPath().toString(), + violation.getInvalidValue() + }, + violation.getMessage(), + null); + } + + @JsonProperty("entity") + public String getEntity() { + return violation.getRootBean().getClass().getName(); + } + + @JsonProperty("message") + public String getMessage() { + return message; + } + + @JsonProperty("invalidValue") + public String getInvalidValue() { + return format("%s", violation.getInvalidValue()); + } + + @JsonProperty("property") + public String getProperty() { + return violation.getPropertyPath().toString(); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExceptionMessage.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExceptionMessage.java new file mode 100644 index 000000000..c9661a8d8 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ExceptionMessage.java @@ -0,0 +1,32 @@ +package org.springframework.data.rest.webmvc.support; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A helper that renders an {@link Exception} JSON-friendly. + * + * @author Jon Brisbin + */ +public class ExceptionMessage { + + private final Throwable exception; + + public ExceptionMessage(Throwable exception) { + this.exception = exception; + } + + @JsonProperty("message") + public String getMessage() { + return exception.getMessage(); + } + + @JsonProperty("cause") + public ExceptionMessage getCause() { + if(null != exception.getCause()) { + return new ExceptionMessage(exception.getCause()); + } + return null; + } + +} + diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java new file mode 100644 index 000000000..5a1a75100 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java @@ -0,0 +1,43 @@ +package org.springframework.data.rest.webmvc.support; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +import org.springframework.data.rest.webmvc.RepositoryRestDispatcherServlet; + +/** + * Helper class to HttpServletRequest helpers. + * + * @author Jon Brisbin + */ +public abstract class HttpRequestUtils { + + /** + * Strip a servlet registration mapping from the request URI. + * + * @param requestUri + * The request URI to strip. + * @param ctx + * The servlet context in which to search for registration mappings. + * + * @return The stripped request URI. + */ + public static String stripRegistrationMapping(String requestUri, + ServletContext ctx) { + for(ServletRegistration reg : ctx.getServletRegistrations().values()) { + if(reg.getClassName().equals(RepositoryRestDispatcherServlet.class.getName()) + || reg.getName().equals("rest-exporter")) { + for(String mapping : reg.getMappings()) { + if(mapping.contains("*")) { + mapping = mapping.substring(0, mapping.indexOf('*')); + } + if(requestUri.startsWith(mapping)) { + return requestUri.replaceAll(mapping, ""); + } + } + } + } + return requestUri; + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/JsonpResponse.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/JsonpResponse.java new file mode 100644 index 000000000..84c539e55 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/JsonpResponse.java @@ -0,0 +1,34 @@ +package org.springframework.data.rest.webmvc.support; + +import org.springframework.http.ResponseEntity; + +/** + * @author Jon Brisbin + */ +public class JsonpResponse { + + private final ResponseEntity responseEntity; + private final String callbackParam; + private final String errbackParam; + + public JsonpResponse(ResponseEntity responseEntity, + String callbackParam, + String errbackParam) { + this.responseEntity = responseEntity; + this.callbackParam = callbackParam; + this.errbackParam = errbackParam; + } + + public ResponseEntity getResponseEntity() { + return responseEntity; + } + + public String getCallbackParam() { + return callbackParam; + } + + public String getErrbackParam() { + return errbackParam; + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSorting.java similarity index 93% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSorting.java index af541a63c..9b7280f30 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PagingAndSorting.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.webmvc; +package org.springframework.data.rest.webmvc.support; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -7,6 +7,7 @@ import java.util.Iterator; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.rest.config.RepositoryRestConfiguration; import org.springframework.web.util.UriComponentsBuilder; /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java new file mode 100644 index 000000000..ca854d63b --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java @@ -0,0 +1,47 @@ +package org.springframework.data.rest.webmvc.support; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.context.MessageSource; +import org.springframework.data.rest.repository.RepositoryConstraintViolationException; +import org.springframework.validation.FieldError; + +/** + * @author Jon Brisbin + */ +public class RepositoryConstraintViolationExceptionMessage { + + private final RepositoryConstraintViolationException violationException; + private final List errors = new ArrayList(); + + public RepositoryConstraintViolationExceptionMessage(RepositoryConstraintViolationException violationException, + MessageSource msgSrc) { + this.violationException = violationException; + + for(FieldError fe : violationException.getErrors().getFieldErrors()) { + List args = new ArrayList(); + args.add(fe.getObjectName()); + args.add(fe.getField()); + args.add(fe.getRejectedValue()); + if(null != fe.getArguments()) { + for(Object o : fe.getArguments()) { + args.add(o); + } + } + + String msg = msgSrc.getMessage(fe.getCode(), + args.toArray(), + fe.getDefaultMessage(), + null); + this.errors.add(msg); + } + } + + @JsonProperty("errors") + public List getErrors() { + return errors; + } + +} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy deleted file mode 100644 index c6610efca..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy +++ /dev/null @@ -1,107 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.codehaus.jackson.map.ObjectMapper -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.ApplicationContext -import org.springframework.data.rest.test.ApplicationConfig -import org.springframework.data.rest.test.ApplicationRestConfig -import org.springframework.data.rest.test.webmvc.Address -import org.springframework.data.rest.test.webmvc.AddressRepository -import org.springframework.data.rest.test.webmvc.CustomerRepository -import org.springframework.data.rest.test.webmvc.Person -import org.springframework.data.rest.test.webmvc.PersonRepository -import org.springframework.data.rest.test.webmvc.ProfileRepository -import org.springframework.data.rest.webmvc.RepositoryRestConfiguration -import org.springframework.data.rest.webmvc.RepositoryRestController -import org.springframework.http.ResponseEntity -import org.springframework.http.server.ServletServerHttpRequest -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.orm.jpa.EntityManagerHolder -import org.springframework.test.context.ContextConfiguration -import org.springframework.transaction.annotation.Transactional -import spock.lang.Specification - -import javax.persistence.EntityManagerFactory - -import static org.springframework.transaction.support.TransactionSynchronizationManager.* - -/** - * @author Jon Brisbin - */ -@ContextConfiguration(classes = [ApplicationConfig, ApplicationRestConfig]) -abstract class BaseSpec extends Specification { - - @Autowired ApplicationContext appCtx - @Autowired RepositoryRestConfiguration config - @Autowired RepositoryRestController controller - @Autowired EntityManagerFactory emf - @Autowired PersonRepository people - @Autowired AddressRepository addresses - @Autowired CustomerRepository customers - @Autowired ProfileRepository profiles - URI baseUri - - def mapper = new ObjectMapper() - - @Transactional - def setup() { - baseUri = URI.create("http://localhost:8080/data") - config.baseUri = baseUri - - if (!hasResource(emf)) { - bindResource(emf, new EntityManagerHolder(emf.createEntityManager())) - } - } - - def readJson(ResponseEntity entity) { - mapper.readValue((byte[]) entity.body, Map) - } - - def createJsonRequest(method, path, query, obj) { - createRequest(method, path, query, "application/json", mapper.writeValueAsString(obj)) - } - - def createUriListRequest(method, path, query, obj) { - createRequest(method, path, query, "text/uri-list", obj.join("\n")) - } - - def createRequest(method, path, query) { - createRequest(method, path, query, null, null) - } - - def createRequest(method, path, query, contentType, content) { - def req = new MockHttpServletRequest( - serverPort: 8080, - requestURI: "/data/$path", - method: method - ) - if (query) { - query.collect { String k, String v -> req.addParameter(k, v)} - } - if (contentType) { - req.contentType = contentType - } - if (content) { - req.content = content - } - - new ServletServerHttpRequest(req) - } - - def newPerson() { - def p = people.save(new Person(name: "John Doe")) - def a = newAddress("Univille") - p.addresses = [a] - people.save(p) - } - - def newAddress(city) { - addresses.save(new Address( - ["1234 W. 1st St."] as String[], - city, - "ST", - "12345" - )) - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy deleted file mode 100644 index 3a7e88548..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy +++ /dev/null @@ -1,51 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.data.domain.PageRequest -import org.springframework.data.rest.webmvc.PagingAndSorting -import org.springframework.data.rest.webmvc.RepositoryRestConfiguration -import org.springframework.http.HttpStatus - -/** - * @author Jon Brisbin - */ -class DiscoverySpec extends BaseSpec { - - def "exposes configured repositories for discovery"() { - - given: - def request = createRequest("GET", "", null) - - when: - def response = controller.listRepositories(request, baseUri) - - then: - response.statusCode == HttpStatus.OK - - when: - def links = readJson(response).links - - then: - links.size() == 6 - - } - - def "lists entities for discovery"() { - - given: - (1..20).each { newPerson() } - def pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 10)) - def request = createRequest("GET", "people", null) - - when: - def response = controller.listEntities(request, pageSort, baseUri, "people") - def body = readJson(response) - - then: - response.statusCode == HttpStatus.OK - body.content.size() == 10 - body.page.totalPages > 1 - body.page.number == 1 - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy deleted file mode 100644 index ce4f91141..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.rest.repository.RepositoryConstraintViolationException -import org.springframework.data.rest.test.webmvc.Customer -import org.springframework.data.rest.test.webmvc.Person -import org.springframework.data.rest.test.webmvc.TestRepositoryEventListener -import org.springframework.http.HttpStatus - -import javax.validation.ConstraintViolationException - -/** - * @author Jon Brisbin - */ -class EventsSpec extends BaseSpec { - - @Autowired TestRepositoryEventListener listener - - def "cannot save invalid entity"() { - - given: - def person = new Person() - def request = createJsonRequest("POST", "people", null, person) - - when: - try { - controller.create(request, baseUri, "people") - } catch (RepositoryConstraintViolationException e) { - controller.handleValidationFailure(e, request) - throw e - } - - then: - thrown(RepositoryConstraintViolationException) - - } - - def "handles JSR-303 validation errors"() { - - given: - def cust = new Customer() - def request = createJsonRequest("POST", "customer", null, cust) - - when: - try { - controller.create(request, baseUri, "customer") - } catch (ConstraintViolationException e) { - controller.handleJsr303ValidationFailure(e, request) - throw e - } - - then: - thrown(ConstraintViolationException) - - } - - def "captures before and after events"() { - - given: - def person = new Person(name: "John Doe") - def request = createJsonRequest("POST", "people", ["returnBody": "true"], person) - def persId - listener.handlers << { evt, p -> - if (evt == "afterSave") - persId = "${p.id}" - } - - when: - def response = controller.create(request, baseUri, "people") - def returnedId = response.headers.getFirst('Location').tokenize("/").last() - - then: - response.statusCode == HttpStatus.CREATED - persId == returnedId - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/JsonpSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/JsonpSpec.groovy deleted file mode 100644 index 15512d7a8..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/JsonpSpec.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.http.HttpStatus - -/** - * @author Jon Brisbin - */ -class JsonpSpec extends BaseSpec { - - def "wraps response with JSONP"() { - - given: - def person = newPerson() - def request = createRequest("GET", "people/${person.id}", ["callback": "jsonp_callback"]) - - when: - def response = controller.entity(request, baseUri, "people", "${person.id}") - def body = new String(response.body) - - then: - response.statusCode == HttpStatus.OK - body?.startsWith("jsonp_callback") - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/NestedObjectSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/NestedObjectSpec.groovy deleted file mode 100644 index 1e2b3ff48..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/NestedObjectSpec.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.data.rest.test.webmvc.Customer -import org.springframework.http.HttpStatus - -/** - * @author Jon Brisbin - */ -class NestedObjectSpec extends BaseSpec { - - def "saves nested object"() { - - given: - def customer = customers.save(new Customer(userid: "jdoe")) - def jsonObj = [ - "customers": [ - ["rel": "customer.Customer", "href": "http://localhost:8080/data/customer/" + customer.id] - ] - ] - def request = createJsonRequest("PUT", "customerTracker/1", null, jsonObj) - def getReq = createRequest("GET", "customerTracker/1/customers", null) - - when: - def response = controller.create(request, baseUri, "customerTracker") - - then: - response.statusCode == HttpStatus.CREATED - - when: - def getResp = controller.propertyOfEntity(getReq, baseUri, "customerTracker", "1", "customers") - def jsonResp = readJson(getResp) - - then: - getResp.statusCode == HttpStatus.OK - jsonResp.content[0].userid == "jdoe" - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy deleted file mode 100644 index a607aef1a..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy +++ /dev/null @@ -1,68 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.data.domain.PageRequest -import org.springframework.data.rest.test.webmvc.Person -import org.springframework.data.rest.webmvc.PagingAndSorting -import org.springframework.data.rest.webmvc.RepositoryRestConfiguration -import org.springframework.http.HttpStatus -import org.springframework.transaction.annotation.Transactional -import spock.lang.Shared - -import java.lang.reflect.InvocationTargetException - -/** - * @author Jon Brisbin - */ -class QueryMethodsSpec extends BaseSpec { - - @Shared - def pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 10)) - - def "exposes query method links to discovery"() { - - given: - def request = createRequest("GET", "people/search", null) - - when: - def response = controller.listQueryMethods(request, baseUri, "people") - def body = readJson(response) - - then: - response.statusCode == HttpStatus.OK - body.links.size() == 5 - - } - - @Transactional - def "invokes query methods"() { - - given: - people.save(new Person(name: "John Doe")) - people.save(new Person(name: "Bill Doe")) - def request = createRequest("GET", "people/search/nameStartsWith", ["name": "John"]) - - when: - def response = controller.query(request, pageSort, baseUri, "people", "nameStartsWith") - def body = readJson(response) - - then: - response.statusCode == HttpStatus.OK - body.content.size() > 0 - - } - - @Transactional - def "blows up on empty query parameters"() { - - given: - def request = createRequest("GET", "people/search/nameStartsWith", null) - - when: - controller.query(request, pageSort, baseUri, "people", "nameStartsWith") - - then: - thrown(InvocationTargetException) - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy deleted file mode 100644 index 8b2f9cb9b..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy +++ /dev/null @@ -1,72 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.data.rest.test.webmvc.Person -import org.springframework.data.rest.test.webmvc.Profile -import org.springframework.http.HttpStatus -import org.springframework.transaction.annotation.Transactional -import org.springframework.web.util.UriComponentsBuilder -import spock.lang.Shared - -/** - * @author Jon Brisbin - */ -class RelationshipsSpec extends BaseSpec { - - @Shared - Long persId - @Shared - Long addrId - @Shared - Long profileId - - def setup() { - def person = people.save(new Person(name: "John Doe")) - persId = person.id - def addr = newAddress("Uniontown") - addrId = addr.id - def profile = profiles.save(new Profile(type: "socialmedia", url: "http://socialmedia.com", person: person)) - person.profiles = ["socialmedia": profile] - people.save(person) - profileId = profile.id - } - - @Transactional - def "saves entity relationship"() { - - given: - def request = createUriListRequest( - "POST", - "people/$persId/addresses", - null, - [UriComponentsBuilder.fromUri(baseUri).pathSegment("address", "$addrId").build().toUriString()] - ) - - when: - def response = controller.updatePropertyOfEntity(request, baseUri, "people", "$persId", "addresses") - - then: - response.statusCode == HttpStatus.CREATED - - when: - request = createRequest("GET", "people/$persId/addresses/$addrId", null) - response = controller.linkedEntity(request, baseUri, "people", "$persId", "addresses", "$addrId") - - then: - response.statusCode == HttpStatus.OK - readJson(response).city == "Uniontown" - - } - - @Transactional - def "cannot delete a required relationship"() { - - when: - def request = createRequest("DELETE", "profile/$profileId/person", null) - def response = controller.clearLinks(request, "profile", "$profileId", "person") - - then: - response.statusCode == HttpStatus.METHOD_NOT_ALLOWED - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy deleted file mode 100644 index e800db32b..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ /dev/null @@ -1,158 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.codehaus.jackson.map.ObjectMapper -import org.springframework.context.support.ClassPathXmlApplicationContext -import org.springframework.data.domain.PageRequest -import org.springframework.data.rest.test.webmvc.Address -import org.springframework.data.rest.webmvc.PagingAndSorting -import org.springframework.data.rest.webmvc.RepositoryRestConfiguration -import org.springframework.data.rest.webmvc.RepositoryRestController -import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration -import org.springframework.http.HttpStatus -import org.springframework.http.server.ServletServerHttpRequest -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockServletConfig -import org.springframework.mock.web.MockServletContext -import org.springframework.orm.jpa.EntityManagerHolder -import org.springframework.transaction.support.TransactionSynchronizationManager -import org.springframework.ui.ExtendedModelMap -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext -import org.springframework.web.util.UriComponentsBuilder -import spock.lang.Ignore -import spock.lang.Shared -import spock.lang.Specification - -import javax.persistence.EntityManagerFactory - -/** - * @author Jon Brisbin - */ -@Ignore -class RepositoryRestControllerSpec extends Specification { - - @Shared - UriComponentsBuilder uriBuilder - @Shared - ObjectMapper mapper = new ObjectMapper() - @Shared - RepositoryRestController controller - @Shared - PagingAndSorting pageSort - @Shared - EntityManagerFactory emf - - MockHttpServletRequest createRequest(String method, String path) { - return new MockHttpServletRequest( - serverPort: 8080, - requestURI: "/data/$path", - method: method - ) - } - - /** - * Try to set up things similarly to how they get loaded in the webapp. - */ - def setupSpec() { - def servletConfig = new MockServletConfig() - def servletContext = new MockServletContext() - - def parentCtx = new ClassPathXmlApplicationContext("classpath*:META-INF/spring-data-rest/**/*-export.xml") - - def webAppCtx = new AnnotationConfigWebApplicationContext() - webAppCtx.servletConfig = servletConfig - webAppCtx.servletContext = servletContext - webAppCtx.configLocations = [RepositoryRestMvcConfiguration.name] as String[] - webAppCtx.parent = parentCtx - webAppCtx.refresh() - - emf = webAppCtx.getBean(EntityManagerFactory) - controller = webAppCtx.getBean(RepositoryRestController) - pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 1000)) - uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data") - } - - def setup() { - if (!TransactionSynchronizationManager.hasResource(emf)) { - TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(emf.createEntityManager())) - } - } - - def "API Test"() { - - given: - def model = new ExtendedModelMap() - - when: "listing available repositories" - def req = createRequest("POST", "people") - def response = controller.listRepositories(new ServletServerHttpRequest(req), uriBuilder) - def reposLinks = mapper.readValue(response.body, Map)?._links - - then: - response.statusCode == HttpStatus.OK - reposLinks?.size() == 4 - - when: "adding an entity" - model.clear() - def data = mapper.writeValueAsBytes([name: "John Doe"]) - req.content = data - response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "people") - - then: - response.statusCode == HttpStatus.CREATED - - when: "getting a specific entity" - model.clear() - req = createRequest("GET", "people/1") - response = controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1") - def entityData = mapper.readValue(response.body, Map) - - then: - entityData?.name == "John Doe" - - when: "updating an entity" - req = createRequest("PUT", "people/1") - data = mapper.writeValueAsBytes([name: "Johnnie Doe", version: 0]) - req.content = data - response = controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "people", "1") - - then: - response.statusCode == HttpStatus.NO_CONTENT - - when: "listing available entities" - response = controller.listEntities(new ServletServerHttpRequest(req), pageSort, uriBuilder, "people") - def selfLink = mapper.readValue(response.body, Map)?.results[0]?._links[2] - - then: - response.statusCode == HttpStatus.OK - selfLink.href == "http://localhost:8080/data/people/1" - - when: "creating a child entity" - req = createRequest("POST", "address") - data = mapper.writeValueAsBytes(new Address(["1 W. 1st St."] as String[], "Univille", "ST", "12345")) - req.content = data - response = controller.create(new ServletServerHttpRequest(req), req, uriBuilder, "address") - - then: - response.statusCode == HttpStatus.CREATED - - when: "linking child to parent entity" - req = createRequest("POST", "people/1/addresses") - req.contentType = "text/uri-list" - data = "http://localhost:8080/data/address/1".bytes - req.content = data - response = controller.updatePropertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses") - - then: - response.statusCode == HttpStatus.CREATED - - when: "getting property of an entity" - response = controller.propertyOfEntity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", "addresses") - def addrLinks = mapper.readValue((byte[]) response.body, Map)?._links - - then: - null != addrLinks - addrLinks.size() == 1 - - } - -} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/ResourceProcessorSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/ResourceProcessorSpec.groovy deleted file mode 100644 index a24e3e1c3..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/ResourceProcessorSpec.groovy +++ /dev/null @@ -1,154 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.core.MethodParameter -import org.springframework.data.rest.test.ApplicationConfig -import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration -import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler -import org.springframework.hateoas.Link -import org.springframework.hateoas.Resource -import org.springframework.hateoas.ResourceProcessor -import org.springframework.test.context.ContextConfiguration -import org.springframework.web.method.support.HandlerMethodReturnValueHandler -import spock.lang.Specification - -/** - * @author Jon Brisbin - */ -@ContextConfiguration(classes = [ApplicationConfig, RepositoryRestMvcConfiguration]) -class ResourceProcessorSpec extends Specification { - - static STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createStringResource"), -1) - static LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createLongResource"), -1) - static SPECIAL_STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialStringResource"), -1) - static SPECIAL_LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialLongResource"), -1) - - HandlerMethodReturnValueHandler delegateHandler - List> processors = [] - boolean handleReturnValueCalled - HandlerMethodReturnValueHandler resourceHandler - - def setup() { - delegateHandler = Mock(HandlerMethodReturnValueHandler) - delegateHandler.handleReturnValue(_, _, null, null) >> { handleReturnValueCalled = true } - - processors << new SpecialStringResourceProcessor() << - new SpecialLongResourceProcessor() << - new StringResourceProcessor() << - new LongResourceProcessor() - - resourceHandler = new ResourceProcessorHandlerMethodReturnValueHandler(delegateHandler, processors) - } - - Resource createStringResource() { - new Resource("string-resource") - } - - Resource createLongResource() { - new Resource(1L) - } - - StringResource createSpecialStringResource() { - new StringResource("special-string-resource") - } - - LongResource createSpecialLongResource() { - new LongResource(1L) - } - - def "processes simple String resource"() { - - given: - def resource = createStringResource() - - when: - resourceHandler.handleReturnValue(resource, STRING_RESOURCE_PARAM, null, null) - - then: - null != resource.getLink("string-resource") - handleReturnValueCalled - - } - - def "process simple Long resource"() { - - given: - def resource = createLongResource() - - when: - resourceHandler.handleReturnValue(resource, LONG_RESOURCE_PARAM, null, null) - - then: - null != resource.getLink("long-resource") - handleReturnValueCalled - - } - - def "process specialized String resource"() { - - given: - def resource = createSpecialStringResource() - - when: - resourceHandler.handleReturnValue(resource, SPECIAL_STRING_RESOURCE_PARAM, null, null) - - then: - null != resource.getLink("special-string-resource") - handleReturnValueCalled - - } - - def "process specialized Long resource"() { - - given: - def resource = createSpecialLongResource() - - when: - resourceHandler.handleReturnValue(resource, SPECIAL_LONG_RESOURCE_PARAM, null, null) - - then: - null != resource.getLink("special-long-resource") - handleReturnValueCalled - - } - -} - -class StringResourceProcessor implements ResourceProcessor> { - @Override Resource process(Resource resource) { - resource.add(new Link("http://localhost:8080/string-resource", "string-resource")) - resource - } -} - -class LongResourceProcessor implements ResourceProcessor> { - @Override Resource process(Resource resource) { - resource.add(new Link("http://localhost:8080/long-resource", "long-resource")) - resource - } -} - -class StringResource extends Resource { - StringResource(String content, Link... links) { - super(content, links) - } -} - -class SpecialStringResourceProcessor implements ResourceProcessor { - @Override StringResource process(StringResource resource) { - resource.add(new Link("http://localhost:8080/special-string-resource", "special-string-resource")) - resource - } -} - -class LongResource extends Resource { - LongResource(Long content, Link... links) { - super(content, links) - } -} - -class SpecialLongResourceProcessor implements ResourceProcessor { - @Override LongResource process(LongResource resource) { - resource.add(new Link("http://localhost:8080/special-long-resource", "special-long-resource")) - resource - } -} \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy deleted file mode 100644 index d3591a40b..000000000 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy +++ /dev/null @@ -1,90 +0,0 @@ -package org.springframework.data.rest.webmvc.spec - -import org.springframework.data.rest.test.webmvc.Person -import org.springframework.http.HttpStatus -import org.springframework.transaction.annotation.Transactional - -/** - * @author Jon Brisbin - */ -class TopLevelEntitySpec extends BaseSpec { - - @Transactional - def "saves top-level entity"() { - - given: - def person = new Person(name: "John Doe") - def request = createJsonRequest("POST", "people/1", null, person) - - when: - def response = controller.createOrUpdate(request, baseUri, "people", "1") - - then: - // Second status given in gradle build but doesn't happen in IDE for some reason - response.statusCode == HttpStatus.CREATED || response.statusCode == HttpStatus.NO_CONTENT - - } - - @Transactional - def "retrieves top-level entity"() { - - given: - def person = newPerson() - def request = createRequest("GET", "people/${person.id}", null) - - when: - def response = controller.entity(request, baseUri, "people", "${person.id}") - - then: - response.statusCode == HttpStatus.OK - - } - - @Transactional - def "updates top-level entity"() { - - given: - def person = newPerson() - person.name = "Johnnie Doe" - person = people.save(person) - def persId = person.id - def request = createJsonRequest("PUT", "people/$persId", null, ["name": "Johnnie Doe"]) - def retrReq = createRequest("GET", "people/$persId", null) - - when: - def response = controller.createOrUpdate(request, baseUri, "people", "$persId") - - then: - response.statusCode == HttpStatus.NO_CONTENT - - when: - response = controller.entity(retrReq, baseUri, "people", "$persId") - - then: - response.statusCode == HttpStatus.OK - - when: - def pers = readJson(response) - - then: - pers.name == "Johnnie Doe" - - } - - @Transactional - def "won't delete entities whose delete methods are not exported"() { - - given: - def person = people.save(new Person(name: "John Doe")) - def persId = person.id - def request = createRequest("DELETE", "people/$persId", null) - - when: - def response = controller.deleteEntity(request, "people", "$persId") - - then: - response.statusCode == HttpStatus.METHOD_NOT_ALLOWED - - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationRestConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationRestConfig.java deleted file mode 100644 index 10d7c76b4..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ApplicationRestConfig.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.springframework.data.rest.test; - -import java.io.IOException; -import java.sql.Timestamp; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; - -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.Version; -import org.codehaus.jackson.map.Module; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.module.SimpleSerializers; -import org.codehaus.jackson.map.ser.std.SerializerBase; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.rest.test.webmvc.Person; -import org.springframework.data.rest.test.webmvc.PersonValidator; -import org.springframework.data.rest.test.webmvc.TestRepositoryEventListener; -import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration; -import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceProcessor; - -/** - * @author Jon Brisbin - */ -@Configuration -@Import(RepositoryRestMvcConfiguration.class) -public class ApplicationRestConfig { - - @SuppressWarnings({"unchecked"}) - @Bean public ConversionService customConversionService() { - DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); - cs.addConverter(new Converter>() { - @Override public List convert(String[] source) { - List longs = new ArrayList(source.length); - for(String s : source) { - longs.add(Long.parseLong(s)); - } - return longs; - } - }); - // cs.addConverter(new Converter() { - // @Override public Resource convert(Person person) { - // Map m = new HashMap(); - // m.put("name", person.getName()); - // CustomResource r = new CustomResource(m); - // r.add(new Link("http://localhost:8080/people/1", "self")); - // return r; - // } - // }); - return cs; - } - - @Bean public ResourceProcessor> personProcessor() { - return new ResourceProcessor>() { - @Override public Resource process(Resource resource) { - System.out.println("\t***** ResourceProcessor for Person: " + resource); - resource.add(new Link("http://localhost:8080/people", "added-link")); - return resource; - } - }; - } - - @Bean public TestRepositoryEventListener testRepositoryEventListener() { - return new TestRepositoryEventListener(); - } - - /** - * This validator will be picked up automatically. The default configuration is to look at the bean name - * and figure out what event you're interested in. This validator is interested in 'beforeSave' events - * because the word 'beforeSave' appears in the first part of the bean name. It recognizes: - *

- * - beforeSave - * - afterSave - * - beforeDelete - * - afterDelete - * - beforeLinkSave - * - afterLinkSave - *

- * What you put after that doesn't matter, you just need to make the bean name unique, of course. - * - * @return - */ - @Bean public PersonValidator beforeSavePersonValidator() { - return new PersonValidator(); - } - - @Bean public Module customModule() { - return new Module() { - private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - - @Override public String getModuleName() { - return "custom"; - } - - @Override public Version version() { - return Version.unknownVersion(); - } - - @Override public void setupModule(SetupContext context) { - context.getDeserializationConfig().withDateFormat(dateFormat); - - SimpleSerializers sers = new SimpleSerializers(); - sers.addSerializer(Timestamp.class, new SerializerBase(Timestamp.class) { - @Override public void serialize(Timestamp value, JsonGenerator jgen, SerializerProvider provider) - throws IOException, JsonGenerationException { - synchronized(dateFormat) { - jgen.writeString(dateFormat.format(value)); - } - } - }); - - context.addSerializers(sers); - } - }; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java deleted file mode 100644 index 61d55f31d..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java +++ /dev/null @@ -1,227 +0,0 @@ -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/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestExporterWebInitializer.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestExporterWebInitializer.java deleted file mode 100644 index a729520c5..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestExporterWebInitializer.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.springframework.data.rest.test; - -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRegistration; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.ContextLoaderListener; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * @author Jon Brisbin - */ -public class RestExporterWebInitializer implements WebApplicationInitializer { - - @Override public void onStartup(ServletContext servletContext) throws ServletException { - // Create the 'root' Spring application context - AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); - rootContext.register(ApplicationConfig.class); - - // Manage the lifecycle of the root application context - servletContext.addListener(new ContextLoaderListener(rootContext)); - - // Register and map the dispatcher servlet - DispatcherServlet servlet = new DispatcherServlet(); - servlet.setContextClass(AnnotationConfigWebApplicationContext.class); - servlet.setContextConfigLocation(ApplicationRestConfig.class.getName()); - ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", servlet); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping("/*"); - - //new DefaultServletHandlerConfigurer(servletContext).enable(); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ValidationErrors.properties b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ValidationErrors.properties deleted file mode 100644 index f9beeb27c..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/ValidationErrors.properties +++ /dev/null @@ -1,2 +0,0 @@ -field.name.required = Field {0}.{1} is required. -no.userid = {0}s must be assigned initial userids. \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java deleted file mode 100644 index a45aba01b..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToOne; - -import org.codehaus.jackson.annotate.JsonBackReference; - -/** - * @author Jon Brisbin - */ -@Entity -public class Address { - - @Id @GeneratedValue private Long id; - private String[] lines; - private String city; - private String province; - private String postalCode; - @JsonBackReference - @OneToOne(cascade = CascadeType.REMOVE) - private Person person; - - 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 Long getId() { - return id; - } - - 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; - } - - public Person getPerson() { - return person; - } - - public void setPerson(Person person) { - this.person = person; - } - - @Override public boolean equals(Object o) { - if(!(o instanceof Address)) { - return false; - } - - Address address2 = (Address)o; - return (address2.id == id || (id != null && id.equals(address2.id))); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java deleted file mode 100644 index db24dd35d..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.Param; - -/** - * @author Jon Brisbin - */ -public interface AddressRepository extends CrudRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AfterSavePersonHandler.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AfterSavePersonHandler.java deleted file mode 100644 index 0ed3ace73..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AfterSavePersonHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.rest.repository.annotation.HandleAfterSave; -import org.springframework.data.rest.repository.annotation.RepositoryEventHandler; -import org.springframework.stereotype.Component; - -/** - * @author Jon Brisbin - */ -@Component -@RepositoryEventHandler(Person.class) -public class AfterSavePersonHandler { - - private final static Logger LOG = LoggerFactory.getLogger(AfterSavePersonHandler.class); - - @HandleAfterSave - public void handleAfterSave(Person person) { - LOG.info("saved person: " + person); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Child.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Child.java deleted file mode 100644 index 719b9c8ed..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Child.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import javax.persistence.Entity; - -/** - * @author Jon Brisbin - */ -@Entity -public class Child extends Parent { - - private String occupation; - - public Child() { - } - - public Child(String name, String occupation) { - super(name); - this.occupation = occupation; - } - - public String getOccupation() { - return occupation; - } - - public void setOccupation(String occupation) { - this.occupation = occupation; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ChildRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ChildRepository.java deleted file mode 100644 index d84ed6a8a..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ChildRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * @author Jon Brisbin - */ -@RestResource(exported = false) -public interface ChildRepository extends JpaRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomResource.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomResource.java deleted file mode 100644 index 012d49e9a..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomResource.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.List; -import java.util.Map; - -import org.codehaus.jackson.annotate.JsonAnyGetter; -import org.codehaus.jackson.annotate.JsonProperty; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; - -/** - * @author Jon Brisbin - */ -public class CustomResource extends Resource> { - - public CustomResource(Map properties) { - super(properties); - } - - @JsonProperty("@id") - public String getSelfLink() { - return super.getId().getHref(); - } - - @JsonProperty("_links") - @Override public List getLinks() { - return super.getLinks(); - } - - @JsonAnyGetter - @Override public Map getContent() { - return super.getContent(); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Customer.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Customer.java deleted file mode 100644 index a36e96a42..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Customer.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.validation.constraints.NotNull; - -/** - * @author Jon Brisbin - */ -@Entity -public class Customer { - - @Id @GeneratedValue private Long id; - @NotNull(message = "no.userid") - private String userid; - - public Long getId() { - return id; - } - - public String getUserid() { - return userid; - } - - public Customer setUserid(String userid) { - this.userid = userid; - return this; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerRepository.java deleted file mode 100644 index b93b46554..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.repository.CrudRepository; - -/** - * @author Jon Brisbin - */ -public interface CustomerRepository extends CrudRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTracker.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTracker.java deleted file mode 100644 index cea10c78c..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTracker.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.Collections; -import java.util.List; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToMany; - -/** - * @author Jon Brisbin - */ -@Entity -public class CustomerTracker { - - @Id @GeneratedValue private Long id; - @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) - private List customers = Collections.emptyList(); - - public Long getId() { - return id; - } - - public List getCustomers() { - return customers; - } - - public CustomerTracker setCustomers(List customers) { - this.customers = customers; - return this; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTrackerRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTrackerRepository.java deleted file mode 100644 index 6ca8e6a0c..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/CustomerTrackerRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.repository.CrudRepository; - -/** - * @author Jon Brisbin - */ -public interface CustomerTrackerRepository extends CrudRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java deleted file mode 100644 index b1834137d..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.List; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.OneToMany; - -/** - * @author Jon Brisbin - */ -@Entity -public class Family { - - @Id @GeneratedValue private Long id; - private String surname; - @OneToMany - private List members; - - public Long getId() { - return id; - } - - public String getSurname() { - return surname; - } - - public void setSurname(String surname) { - this.surname = surname; - } - - public List getMembers() { - return members; - } - - public void setMembers(List members) { - this.members = members; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java deleted file mode 100644 index 96ebc1492..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.repository.CrudRepository; - -/** - * @author Jon Brisbin - */ -public interface FamilyRepository - extends CrudRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Parent.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Parent.java deleted file mode 100644 index 68b2de681..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Parent.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; - -/** - * @author Jon Brisbin - */ -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -public class Parent { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - - public Parent() { - } - - public Parent(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ParentRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ParentRepository.java deleted file mode 100644 index 0bd4ac00f..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ParentRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * @author Jon Brisbin - */ -@RestResource(exported = false) -public interface ParentRepository extends JpaRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java deleted file mode 100644 index c5c2fe4a3..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Map; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.MapKey; -import javax.persistence.OneToMany; -import javax.persistence.PrePersist; -import javax.persistence.Version; - -import org.codehaus.jackson.annotate.JsonManagedReference; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * @author Jon Brisbin - */ -@Entity -public class Person { - - @Id @GeneratedValue private Long id; - private String name; - @RestResource(path = "version") - @Version - private Long version; - @JsonManagedReference - @OneToMany(cascade = CascadeType.REMOVE) - private List

addresses; - @OneToMany(cascade = CascadeType.REMOVE) - @MapKey(name = "type") - private Map profiles; - private Date created; - - 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 Person(String name, Map profiles) { - this.name = name; - this.profiles = profiles; - } - - public Person(String name) { - this.name = name; - } - - 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; - } - - public Date getCreated() { - return created; - } - - @PrePersist - private void setCreated() { - this.created = Calendar.getInstance().getTime(); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java deleted file mode 100644 index a7a62dcd5..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Jon Brisbin - */ -@Component -public class PersonLoader implements InitializingBean { - - @Autowired - private PersonRepository personRepository; - @Autowired - private ProfileRepository profileRepository; - @Autowired - private AddressRepository addressRepository; - - @Transactional - @Override public void afterPropertiesSet() - throws Exception { - - Person p1 = personRepository.save(new Person("John Doe")); - - Map pers1profiles = new HashMap(); - Profile twitter = profileRepository.save(new Profile("twitter", "#!/johndoe", p1)); - Profile fb = profileRepository.save(new Profile("facebook", "/johndoe", p1)); - pers1profiles.put("twitter", twitter); - pers1profiles.put("facebook", fb); - p1.setProfiles(pers1profiles); - - Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."}, - "Univille", - "ST", - "12345")); - p1.setAddresses(Arrays.asList(pers1addr)); - - personRepository.save(p1); - - - Person p2 = personRepository.save(new Person("Jane Doe")); - - Map pers2profiles = new HashMap(); - Profile twitter2 = profileRepository.save(new Profile("twitter", "#!/janedoe", p2)); - Profile fb2 = profileRepository.save(new Profile("facebook", "/janedoe", p2)); - pers2profiles.put("twitter", twitter2); - pers2profiles.put("facebook", fb2); - p2.setProfiles(pers2profiles); - - Address pers2addr = addressRepository.save(new Address(new String[]{"1234 E. 2nd St."}, - "Univille", - "ST", - "12345")); - p2.setAddresses(Arrays.asList(pers2addr)); - - personRepository.save(p2); - - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java deleted file mode 100644 index 64581ed4c..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.Date; -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.repository.query.Param; -import org.springframework.data.rest.repository.annotation.ConvertWith; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * Example {@link org.springframework.data.repository.CrudRepository} for dealing with a {@link Person}. Also uses the - * {@link RestResource} annotation to turn off the delete methods. - * - * @author Jon Brisbin - */ -@RestResource(path = "people", rel = "people") -public interface PersonRepository extends PagingAndSortingRepository { - - @Override - @RestResource(exported = false) void delete(Long id); - - @Override - @RestResource(exported = false) void delete(Person entity); - - @RestResource(path = "name", rel = "names") List findByName(@Param("name") String name); - - @RestResource(path = "nameStartsWith", rel = "nameStartsWith") - Page findByNameStartsWith(@Param("name") String name, Pageable p); - - @Query("select count(p) from Person p") - @RestResource(path = "count") Long personCount(); - - @Query("select p from Person p where p.id in(:ids)") - @RestResource(path = "id") Page findById(@Param("ids") List ids, Pageable pageable); - - @RestResource(path = "created") List findByCreatedGreaterThan( - @Param("startDate") @ConvertWith(StringToISODateConverter.class) Date startDate - ); - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java deleted file mode 100644 index 245b9511f..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.ClassUtils; -import org.springframework.validation.Errors; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; - -/** - * @author Jon Brisbin - */ -public class PersonValidator implements Validator { - - private static final Logger LOG = LoggerFactory.getLogger(PersonValidator.class); - - @Override public boolean supports(Class clazz) { - return ClassUtils.isAssignable(clazz, Person.class); - } - - @Override public void validate(Object target, Errors errors) { - Person p = (Person)target; - LOG.debug(" ***** Validating Person " + p); - ValidationUtils.rejectIfEmpty(errors, "name", "field.name.required"); - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java deleted file mode 100644 index 379d79246..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.ManyToOne; - -import org.codehaus.jackson.annotate.JsonBackReference; - -/** - * @author Jon Brisbin - */ -@Entity -public class Profile { - - @Id @GeneratedValue private Long id; - private String type; - private String url; - @JsonBackReference - @ManyToOne(optional = false) - private Person person; - - public Profile() { - } - - public Profile(String type, String url) { - this.type = type; - this.url = url; - } - - public Profile(String type, String url, Person person) { - this.type = type; - this.url = url; - this.person = person; - } - - 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; - } - - public Person getPerson() { - return person; - } - - public void setPerson(Person person) { - this.person = person; - } - - @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/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java deleted file mode 100644 index 9dc8ccd55..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.Param; - -/** - * @author Jon Brisbin - */ -public interface ProfileRepository extends CrudRepository { - - public Address findByPerson(@Param("person") Person person); - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java deleted file mode 100644 index 918fe0417..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2012 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.rest.test.webmvc; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.internal.matchers.Equals; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.core.MethodParameter; -import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceProcessor; -import org.springframework.hateoas.Resources; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * Unit tests for {@link ResourceProcessorHandlerMethodReturnValueHandler}. - * - * @author Oliver Gierke - */ -@RunWith(MockitoJUnitRunner.class) -public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { - - @Mock - HandlerMethodReturnValueHandler delegate; - - @Mock - MethodParameter parameter; - - List> resourceProcessors; - - Resource source = new Resource("foo"); - Resource result = StringResourceProcessor.RESULT; - - @Before - public void setUp() { - resourceProcessors = new ArrayList>(); - } - - @Test - public void supportsIfDelegateSupports() { - assertSupport(true); - } - - @Test - public void doesNotSupportIfDelegateDoesNot() { - assertSupport(false); - } - - private void assertSupport(boolean value) { - - when(delegate.supportsReturnType(Mockito.any(MethodParameter.class))).thenReturn(value); - HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate, - resourceProcessors); - - assertThat(handler.supportsReturnType(parameter), is(value)); - } - - @Test - public void invokesStringPostProcessorForSimpleStringResource() throws Exception { - - resourceProcessors.add(new StringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - HttpEntity> input = new HttpEntity>(source); - HttpEntity> output = new HttpEntity>(result); - - assertProcessorInvokedForMethod("stringResourceEntity", input, output); - assertProcessorInvokedForMethod("resourceEntity", input, output); - } - - @Test - public void invokesStringPostProcessorForSimpleStringResourceInResponseEntity() throws Exception { - - resourceProcessors.add(new StringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - ResponseEntity> input = new ResponseEntity>(source, HttpStatus.OK); - ResponseEntity> output = new ResponseEntity>(result, HttpStatus.OK); - - assertProcessorInvokedForMethod("stringResourceEntity", input, output); - assertProcessorInvokedForMethod("resourceEntity", input, output); - } - - @Test - public void invokesStringPostProcessorForSimpleStringResources() throws Exception { - - resourceProcessors.add(new StringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - resourceProcessors.add(new StringResourcesProcessor()); - - Resources> sources = new Resources>(Collections.singleton(source)); - - HttpEntity>> input = new HttpEntity>>(sources); - HttpEntity>> output = new HttpEntity>>( - StringResourcesProcessor.RESULT); - - assertProcessorInvokedForMethod("stringResourceEntity", input, output); - assertProcessorInvokedForMethod("resourceEntity", input, output); - } - - @Test - public void invokesStringPostProcessorForSpecializedStringResource() throws Exception { - - resourceProcessors.add(new StringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - HttpEntity> stringOutput = new HttpEntity>(result); - HttpEntity specializedInput = new HttpEntity(new StringResource("foo")); - - assertProcessorInvokedForMethod("stringResourceEntity", specializedInput, stringOutput); - assertProcessorInvokedForMethod("resourceEntity", specializedInput, stringOutput); - assertProcessorInvokedForMethod("specializedStringResourceEntity", specializedInput, stringOutput); - } - - @Test - public void doesNotInvokeSpecializedStringPostProcessorForSimpleStringResource() throws Exception { - - resourceProcessors.add(new SpecializedStringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - HttpEntity> input = new HttpEntity>(source); - - assertProcessorInvokedForMethod("stringResourceEntity", input, input); - } - - @Test - public void invokesSpecializedStringPostProcessor() throws Exception { - - resourceProcessors.add(new SpecializedStringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - HttpEntity input = new HttpEntity(new StringResource("foo")); - HttpEntity output = new HttpEntity(SpecializedStringResourceProcessor.RESULT); - - assertProcessorInvokedForMethod("specializedStringResourceEntity", input, output); - } - - @Test - public void invokesLongPostProcessorForLongResource() throws Exception { - - resourceProcessors.add(new StringResourceProcessor()); - resourceProcessors.add(new LongResourceProcessor()); - - HttpEntity> input = new HttpEntity>(new Resource(50L)); - HttpEntity specializedInput = new HttpEntity(new LongResource(50L)); - HttpEntity> output = new HttpEntity>(LongResourceProcessor.RESULT); - - assertProcessorInvokedForMethod("resourceEntity", specializedInput, output); - assertProcessorInvokedForMethod("numberResourceEntity", input, output); - } - - private void assertProcessorInvokedForMethod(String methodName, Object returnValue, Object processedValue) - throws Exception { - - HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate, - resourceProcessors); - - Method method = Controller.class.getMethod(methodName); - MethodParameter returnType = new MethodParameter(method, -1); - - handler.handleReturnValue(returnValue, returnType, null, null); - - verify(delegate, times(1)).handleReturnValue(argThat(new HttpEntityMatcher(processedValue)), eq(returnType), - eq((ModelAndViewContainer) null), eq((NativeWebRequest) null)); - } - - @SuppressWarnings("serial") - static class HttpEntityMatcher extends Equals { - - public HttpEntityMatcher(Object wanted) { - super(wanted); - } - - /* (non-Javadoc) - * @see org.mockito.internal.matchers.Equals#matches(java.lang.Object) - */ - @Override - public boolean matches(Object actual) { - - Object wanted = getWanted(); - - if (actual instanceof ResponseEntity && wanted instanceof ResponseEntity) { - - ResponseEntity left = (ResponseEntity) wanted; - ResponseEntity right = (ResponseEntity) actual; - - if (!left.getStatusCode().equals(right.getStatusCode())) { - return false; - } - } - - if (actual instanceof HttpEntity && wanted instanceof HttpEntity) { - - HttpEntity left = (HttpEntity) wanted; - HttpEntity right = (HttpEntity) actual; - - if (!left.getBody().equals(right.getBody())) { - return false; - } - - if (!left.getHeaders().equals(right.getHeaders())) { - return false; - } - - return true; - } - - return super.matches(actual); - } - } - - interface Controller { - - Resources> resources(); - - Resource resource(); - - StringResource specializedResource(); - - Object object(); - - HttpEntity> resourceEntity(); - - HttpEntity> resourcesEntity(); - - HttpEntity objectEntity(); - - HttpEntity> stringResourceEntity(); - - HttpEntity> numberResourceEntity(); - - HttpEntity specializedStringResourceEntity(); - - ResponseEntity> resourceResponseEntity(); - - ResponseEntity> resourcesResponseEntity(); - } - - /** - * {@link ResourceProcessor} to process {@link String}s. - * - * @author Oliver Gierke - */ - static class StringResourceProcessor implements ResourceProcessor> { - - static final Resource RESULT = new Resource("bar"); - - @Override - public Resource process(Resource resource) { - return RESULT; - } - } - - static class StringResourcesProcessor implements ResourceProcessor>> { - - static final Resources> RESULT = new Resources>( - Collections.singleton(StringResourceProcessor.RESULT)); - - @Override - public Resources> process(Resources> resources) { - return RESULT; - } - } - - /** - * {@link ResourceProcessor} to process {@link Long} values. - * - * @author Oliver Gierke - */ - static class LongResourceProcessor implements ResourceProcessor> { - - static final Resource RESULT = new Resource(10L); - - @Override - public Resource process(Resource resource) { - return RESULT; - } - } - - static class StringResource extends Resource { - - public StringResource(String value) { - super(value); - } - } - - static class LongResource extends Resource { - - public LongResource(Long value) { - super(value); - } - } - - static class SpecializedStringResourceProcessor implements ResourceProcessor { - - static final StringResource RESULT = new StringResource("foobar"); - - @Override - public StringResource process(StringResource resource) { - return RESULT; - } - } -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToISODateConverter.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToISODateConverter.java deleted file mode 100644 index 935def3ec..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToISODateConverter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import org.springframework.core.convert.converter.Converter; - -/** - * @author Jon Brisbin - */ -public class StringToISODateConverter implements Converter { - @Override public Date convert(String[] s) { - if(s.length == 1) { - try { - return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse(s[0]); - } catch(ParseException e) { - throw new IllegalArgumentException(e); - } - } - - throw new IllegalArgumentException("Can only parse a single date in the parameter."); - } -} \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java deleted file mode 100644 index 83c632d02..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.util.StringUtils; - -/** - * @author Jon Brisbin - */ -public class StringToListOfLongsConverter implements Converter> { - - @Override public List convert(String[] source) { - List longs = new ArrayList(); - String strings; - if(source.length == 1) { - strings = source[0]; - } else { - strings = StringUtils.arrayToCommaDelimitedString(source); - } - for(String s : StringUtils.commaDelimitedListToStringArray(strings)) { - longs.add(Long.parseLong(s)); - } - return longs; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java deleted file mode 100644 index 8d00e4abe..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.ArrayList; -import java.util.List; - -import groovy.lang.Closure; -import org.springframework.data.rest.repository.context.AbstractRepositoryEventListener; - -/** - * @author Jon Brisbin - */ -public class TestRepositoryEventListener extends AbstractRepositoryEventListener { - - private List handlers = new ArrayList(); - - public List getHandlers() { - return handlers; - } - - @Override protected void onBeforeSave(Object entity) { - for(Closure cl : handlers) { - cl.call("beforeSave", entity); - } - } - - @Override protected void onAfterSave(Object entity) { - for(Closure cl : handlers) { - cl.call("afterSave", entity); - } - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTest.java deleted file mode 100644 index d08512912..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.UUID; -import javax.persistence.Entity; -import javax.persistence.Id; - -/** - * @author Jon Brisbin - */ -@Entity -public class UuidTest { - - @Id UUID id = UUID.randomUUID(); - String name; - - public UuidTest() { - } - - public UUID getId() { - return id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java deleted file mode 100644 index e3fe4fe1f..000000000 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.springframework.data.rest.test.webmvc; - -import java.util.UUID; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.rest.repository.annotation.RestResource; - -/** - * @author Jon Brisbin - */ -@RestResource(exported = false) -public interface UuidTestRepository - extends CrudRepository { -} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractServerEnabledTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractServerEnabledTest.java new file mode 100644 index 000000000..c000b7f98 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractServerEnabledTest.java @@ -0,0 +1,23 @@ +package org.springframework.data.rest.webmvc; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.Before; +import org.junit.BeforeClass; + +/** + * @author Jon Brisbin + */ +public abstract class AbstractServerEnabledTest { + + private Server server; + + @Before + public void setup() { + if(null == server) { + server = new Server(0); + + } + } + +} diff --git a/spring-data-rest-webmvc/src/test/resources/META-INF/persistence.xml b/spring-data-rest-webmvc/src/test/resources/META-INF/persistence.xml deleted file mode 100644 index 247d7db3d..000000000 --- a/spring-data-rest-webmvc/src/test/resources/META-INF/persistence.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - org.springframework.data.rest.test.webmvc.Address - org.springframework.data.rest.test.webmvc.Child - org.springframework.data.rest.test.webmvc.Customer - org.springframework.data.rest.test.webmvc.CustomerTracker - org.springframework.data.rest.test.webmvc.Family - org.springframework.data.rest.test.webmvc.Person - org.springframework.data.rest.test.webmvc.Profile - org.springframework.data.rest.test.webmvc.UuidTest - - - - - - - - - - \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml b/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml deleted file mode 100644 index 1e1f13c8f..000000000 --- a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/load_data.sh b/spring-data-rest-webmvc/src/test/resources/load_data.sh deleted file mode 100755 index e920ad070..000000000 --- a/spring-data-rest-webmvc/src/test/resources/load_data.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -curl -v -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family -curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people -curl -v -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/people -curl -v -d 'http://localhost:8080/people/1 -http://localhost:8080/people/2' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members -curl -v -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/1"}}' -H "Content-Type: application/json" http://localhost:8080/address -curl -v -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses -curl -v -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person -curl -v -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille","person": {"href":"http://localhost:8080/people/2"}}' -H "Content-Type: application/json" http://localhost:8080/address -curl -v -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses -curl -v -d '{"type" : "twitter", "url": "#!/johndoe", "person": {"href": "http://localhost:8080/people/1"}}' -H "Content-Type: application/json" http://localhost:8080/profile -#curl -v -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/profiles -curl -v -d '{"type" : "facebook", "url": "/janedoe", "person": {"href": "http://localhost:8080/people/2"}}' -H "Content-Type: application/json" http://localhost:8080/profile -#curl -v -d '{"links": [{"rel":"facebook", "href": "http://localhost:8080/profile/2"}]}' -H "Content-Type: application/json" http://localhost:8080/people/2/profiles \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/load_name_data.rb b/spring-data-rest-webmvc/src/test/resources/load_name_data.rb deleted file mode 100644 index fd584aba2..000000000 --- a/spring-data-rest-webmvc/src/test/resources/load_name_data.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "json" -require "net/http" - -client = Net::HTTP.new("localhost", 8080) - -File.open("names.txt").each_line do |name| - client.post("/people", JSON.dump({"name" => name.chomp}), {"Content-Type"=>"application/json"}) -end diff --git a/spring-data-rest-webmvc/src/test/resources/names.txt b/spring-data-rest-webmvc/src/test/resources/names.txt deleted file mode 100644 index 831053089..000000000 --- a/spring-data-rest-webmvc/src/test/resources/names.txt +++ /dev/null @@ -1,100 +0,0 @@ -Adalberto Raymos -Adrien Maytubby -Alonzo Schroyer -Alva Sauvageau -Amalia Velie -Amber Pay -Annabell Zozaya -Antone Ryan -Antonia Maslanka -Art Esperanza -Ashlee Mittan -Audrie Smid -Augustine Crosswell -Benny Graden -Billye Bornmann -Blythe Milby -Bret Pistole -Briana Angry -Bruno Feeley -Carol Cruikshank -Chanell Neidlinger -Cher Griswould -Cheri Batson -Claud Bardon -Crysta Kooker -Cyrus Balius -Daniel Vangieson -Daron Gardocki -Delana Rowley -Devon Osei -Diego Schaefer -Dionna Chavers -Doretha Folden -Edna Codner -Elfreda Capron -Eli Ekhoff -Elijah Canard -Emile Steenburg -Erline Santiago -Ervin Kennemore -Ezekiel Clinkenbeard -Felisa Burmeister -Fleta Mckiney -Frankie Mires -Gaston Spille -Gerardo Mandiola -Gilda Wilbers -Guadalupe Boutiette -Hannah Perloff -Jeanna Rundstrom -Kandis Netherland -Keneth Sigg -Kenton Layssard -Kimberlee Turlington -Kimiko Corlew -Latricia Fickas -Leanna Wedel -Lindsey Mccalister -Lucas Trischitta -Marhta Genther -Mathew Garramone -Maxie Coke -Micheal Veronesi -Miguel Eveland -Ona Hardrick -Orville Mccarson -Raguel Moscowitz -Rana Bussmann -Rashad Deuser -Renata Labate -Reva Larger -Rey Durtschi -Rosalina Merthie -Rusty Biafore -Samual Moree -Samual Plattsmier -Scot Cheely -Seymour Kohls -Shaniqua Khan -Shanon Kueny -Sharmaine Musel -Shelby Prator -Sheldon Loiselle -Shenita Broxterman -Silas Scarth -Sol Stockfisch -Sonia Otsuka -Stephine Daum -Stewart Lenzo -Theron Carmell -Titus Streck -Tomi Minasian -Tyrone Hopton -Usha Horsely -Val Hoffmeyer -Vicente Perko -Virgil Cousin -Walton Svoboda -Winford Hagwood -Yoshiko Dekany diff --git a/src/api/overview.html b/src/api/overview.html new file mode 100644 index 000000000..f03738efd --- /dev/null +++ b/src/api/overview.html @@ -0,0 +1,6 @@ + + + +Spring Data REST HATEOAS-friendly resouce exporter. + + \ No newline at end of file diff --git a/src/api/spring-javadoc.css b/src/api/spring-javadoc.css new file mode 100644 index 000000000..191be29d1 --- /dev/null +++ b/src/api/spring-javadoc.css @@ -0,0 +1,184 @@ +/* + * Copyright 2010 SpringSource + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.code +{ + border: 1px solid black; + background-color: #F4F4F4; + padding: 5px; +} + +body +{ + font: 12px Verdana, Arial, Helvetica, "Bitstream Vera Sans", sans-serif; + background-color: #fff; + color: #333; +} + + +/* Link colors */ +a +{ + color:#2c7b14; + text-decoration:none; +} + +a:hover +{ + text-decoration:underline; +} + +/* Headings */ +h1 +{ + font-size:28px; + color:#007c00; +} + +/* Table colors */ + +table +{ + border:none; +} + +td +{ + border:none; + border-bottom:1px dotted #ddd; +} + +th +{ + border:none; +} + +.TableHeadingColor th +{ + background-color: #efffcb; + background-image: url(resources/TableHeading-background.png); + background-repeat: repeat-x; + color:#fff; + font-size:14px; + height:26px; +} + +.TableSubHeadingColor +{ + background: #f7ffee; + +} +.TableRowColor +{ + background: #fff; +} + +.TableRowColor a +{ + border-bottom:none; + color:#2c7b14; + font-weight:normal; +} + +tr.TableRowColor:hover +{ + background:#eef2e1; +} + + +/* Font used in left-hand frame lists */ +.FrameTitleFont +{ + font-size: 120%; + font-weight:bold; +} + +.FrameTitleFont a +{ + color: #333; +} + +.FrameHeadingFont +{ + font-weight: bold; + font-size:95%; +} + +.FrameItemFont +{ + line-height:130%; + font-size: 95%; +} + +.FrameItemFont a +{ + color:#333; +} + +.FrameItemFont a:hover +{ + color:#249901; + border-bottom:none; + text-decoration:underline; +} + +/* Navigation bar fonts and colors */ +.NavBarCell1 +{ + background-color:#fff; + border:none; +} + +.NavBarCell1Rev +{ + background-color:#e3faa5; + border:1px solid #9ad00c; + padding:0; + margin:0; +} + +.NavBarCell1 a +{ + color:#333; + text-decoration:none; +} + +.NavBarFont1Rev +{ + +} + +.NavBarCell2 +{ + border:none; +} + +.NavBarCell2 a +{ + color:#249901; + font-size:90%; +} + +.NavBarCell3 +{ + border:none; +} + +/* Override sizes in font tags */ +font +{ + font: inherit !important; +} diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/dist/license.txt b/src/dist/license.txt new file mode 100644 index 000000000..9ffae4109 --- /dev/null +++ b/src/dist/license.txt @@ -0,0 +1,279 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================= + +SPRING FRAMEWORK ${version} SUBCOMPONENTS: + +Spring Framework ${version} includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 4.0 (org.ow2.asm:asm:4.0, org.ow2.asm:asm-commons:4.0): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.0 (cglib:cglib:3.0): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +http://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download, +CGLIB 3.0 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +======================================================================= + +To the extent any open source subcomponents are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from http://www.springsource.org/download, or by +sending a request, with your name and address to: + + VMware, Inc., 3401 Hillview Avenue + Palo Alto, CA 94304 + United States of America + +or email info@vmware.com. All such requests should clearly specify: + + OPEN SOURCE FILES REQUEST + Attention General Counsel + +VMware shall mail a copy of the Source Files to you on a CD or equivalent +physical medium. This offer to obtain a copy of the Source Files is valid for +three years from the date you acquired this Software product. \ No newline at end of file diff --git a/src/dist/notice.txt b/src/dist/notice.txt new file mode 100644 index 000000000..57c97ba65 --- /dev/null +++ b/src/dist/notice.txt @@ -0,0 +1,11 @@ +Spring Data REST ${version} +Copyright (c) 2012-${copyright} SpringSource, a division of VMware, Inc. + +This product is licensed to you under the Apache License, Version 2.0 +(the "License"). You may not use this product except in compliance with +the License. + +This product may include a number of subcomponents with separate +copyright notices and license terms. Your use of the source code for +these subcomponents is subject to the terms and conditions of the +subcomponent's license, as noted in the license.txt file. \ No newline at end of file diff --git a/src/reference/docbook/index.xml b/src/reference/docbook/index.xml new file mode 100644 index 000000000..e6e1da941 --- /dev/null +++ b/src/reference/docbook/index.xml @@ -0,0 +1,53 @@ + + + + + Spring Data REST Reference Documentation + Spring Data REST + + ${version} + + + + + Jon + Brisbin + + + + + Oliver + Gierke + + + + + + 2012-2013 + + + + Copies of this document may be made for your own use and for + distribution to others, provided that you do not charge any fee for such + copies and further provided that each copy contains this Copyright + Notice, whether distributed in print or electronically. + + + + + + + + Spring Data REST Introduction + + + + + \ No newline at end of file diff --git a/src/reference/docbook/intro.xml b/src/reference/docbook/intro.xml new file mode 100644 index 000000000..709a7357b --- /dev/null +++ b/src/reference/docbook/intro.xml @@ -0,0 +1,16 @@ + + + + Spring Data REST Introduction + +
+ Overview of Spring Data REST features + + +
+ +
\ No newline at end of file