commit 21b1a4ec8cd76653fc49a6d7bc6b2c38a6d7685c Author: Oliver Gierke Date: Thu May 10 20:25:31 2012 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5be812db --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +.settings/ +.project +.classpath \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..8862a0b1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + 4.0.0 + + org.springframework.hateoas + spring-hateoas + 1.0.0.BUILD-SNAPSHOT + + Spring Hateoas + + Library to support implementing represnetaitons for + hyper-text driven REST web services. + + + 2012 + + + SpringSource, a division of VMware + http://www.springsource.org + + + + + ogierke + Oliver Gierke + ogierke(at)vmware.com + SpringSource, a division of VMware + + Project lead + + +1 + + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Copyright 2011 the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + + + 3.1.1.RELEASE + + + + + org.springframework + spring-core + ${spring.version} + + + + org.springframework + spring-webmvc + ${spring.version} + + + javax.servlet + servlet-api + 2.5 + provided + + + + org.hamcrest + hamcrest-library + 1.2.1 + test + + + + junit + junit-dep + 4.10 + test + + + + org.springframework + spring-test + ${spring.version} + test + + + + org.mockito + mockito-all + 1.9.0 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..cf585e06 --- /dev/null +++ b/readme.md @@ -0,0 +1,144 @@ +# Spring HATEOAS +This project provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly. + +## JAXB / JSON integration +As representations for REST web services are usually rendered in either XML or JSON the natural choice of technology to achieve this is either JAXB, JSON or both in combination. To follow HATEOAS principles you need to incorporate links into those representation. Spring HATEOAS provides a set of useful types to ease working with those. + +## Links +The `Link` value object follows the Atom link definition and consists of a `del` and an `href` attribute. It contains a few constants for well known reels such as `self`, `next` etc. The XML representation will render in the Atom namespace. + + Link link = new Link("http://localhost:8080/something"); + assertThat(link.getHref(), is("http://localhost:8080/something")); + assertThat(link.getRel(), is(Link.SELF)); + + Link link = new Link("http://localhost:8080/something", "my-rel"); + assertThat(link.getHref(), is("http://localhost:8080/something")); + assertThat(link.getRel(), is("my-rel")); + +## Resources +As pretty much every representation of a resource will contain some links (at least the `self` one) we provide a base class to actually inherit from when designing representation classes. + + class PersonResource extends ResourceSupport { + + String firstname; + String lastname; + } + +Inheriting from `ResourceSupport` will allow adding links easily: + + PersonResource resource = new PersonResource(); + resource.firstname = "Dave"; + resource.lastname = "Matthews"; + resource.add(new Link("http://myhost/people")); + +This would render as follows in JSON: + + { firstname : "Dave", + lastname : "Matthews", + _links : [ { del : "self", href : "http://myhost/people" } ] } + +… or slightly more verbose in XML … + + + Dave + Matthews + + + + + +You can also easily access links contained in that resource: + + Link selfLink = new Link("http://myhost/people"); + assertThat(resource.getId(), is(selfLink); + assertThat(resource.getLink(Link.SELF), is(selfLink)); + +## Link builder +Now we've got the domain vocabulary in place, but the main challenge remains: how to create the actual URIs to be wrapped into `Link`s in a less fragile way. Right now we'd have to duplicate URI strings all over the place which is brittle and unmaintainable. + +Assume you have your Spring MVC controllers implemented as follows: + + @Controller + @RequestMapping("/people") + class PersonController { + + @RequestMapping(method = RequestMethod.GET) + public HttpEntity showAll() { + + } + + @RequestMapping(value = "/{person}", method = RequestMethod.GET) + public HttpEntity show(@PathVariable Long person) { + + } + } + +We see two conventions here. There's a collection resource exposed through the controller class' `@RequestMapping` annotation with individual elements of that collections exposed as direct sub resource. The collection resource might be exposed at a simple URI (as just shown) or more complex ones like `/people/{id}/addresses`. +Let's say you would like to actually link to the collection resource of all people. Following the approach from up above would cause two problems: + +1. To create an absolute URI you'd need to lookup the protocol, hostname, port, servlet base etc. This is cumbersome and requires ugly manual string concatenation code. +2. You probably don't want to concatenate the `/people` on top of your base URI because you'd have to maintain the information in multiple places then. Change the mapping, change all the clients pointing to it. + +Spring HATEOS now provides a ControllerLinkBuilder that allows to create links by pointing to controller classes: + + import static org.sfw.hateoas.mvc.ControllerLinkBuilder.*; + + Link link = linkTo(PersonController.class).withRel("people"); + assertThat(link.getRel(), is("people")); + assertThat(link.getHref(), endsWith("/people")); + +You can now easily build more nested links as well: + + Person person = new Person(1L, "Dave", "Matthews"); + // /person / 1 + Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel(); + assertThat(link.getRel(), is(Link.SELF)); + assertThat(link.getHref(), endsWith("/people/1")); + +If your domain class implements the `Identifiable` interface the `slash(…)` method will rather invoke `getId()` on the given object instead of `toString()`. Thus the just shown link creation can be abbreviated to: + + class Person implements Identifiable { + public Long getId() { … } + } + + Link link = linkTo(PersonController.class).slash(person).withSelfRel(); + +The builder also allows creating URI instances to build up e.g. response header values: + + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(linkTo(PersonController.class).slash(person).toUri()); + return new ResponseEntity(headers, HttpStatus.CREATED); + +## Resource assembler +As the mapping from an entity to a resource type will have to be used in multiple places it makes sense to create a dedicated class responsible for doing so. The conversion will of course contain very custom steps but also a few boilerplate ones: + +1. Instantiation of the resource class +2. Adding a link with del `self` pointing to the resource that gets rendered. + +Spring HATEOAS now provides a `ResourceAssemblerSupport` base class that helps reducing the amount of code needed to be written: + + class PersonResourceAssembler extends ResourceAssemblerSupport { + + public PersonResourceAssembler() { + super(PersonController.class, PersonResource.class); + } + + @Override + public PersonResource toResource(Person person) { + + PersonResource resource = createResource(person); + // … do further mapping + return resource; + } + } + +Setting the class up like this gives you the following benefits: there are a hand full of `createResource(…)` methods that will allow you to create an instance of the resource and have it a `Link` with a del of `self` added to it. The href of that link is determined by the configured controllers request mapping plus the id of the `Identifiable` (e.g. `/people/1` in our case). The resource type gets instantiated by reflection and expects a no-arg constructor. Simply override `instantiateResource(…)` in case you'd like to use a dedicated constructor or avoid the reflection performance overhead. + +The assembler can then be used to either assemble a single resource or an `Iterable` of them: + + Person person = new Person(…); + Iterable people = Collections.singletonList(person); + + PersonResourceAssembler assembler = new PersonResourceAssembler(); + PersonResource resource = assembler.toResource(person); + List resources = assembler.toResource(people); \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/Identifiable.java b/src/main/java/org/springframework/hateoas/Identifiable.java new file mode 100644 index 00000000..e8f8488d --- /dev/null +++ b/src/main/java/org/springframework/hateoas/Identifiable.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.hateoas; + +import java.io.Serializable; + +/** + * Interface to mark objects that are identifiable by an ID of any type. + * + * @author Oliver Gierke + */ +public interface Identifiable { + + /** + * Returns the id identifying the object. + * + * @return the identifier or {@literal null} if not available. + */ + ID getId(); +} diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java new file mode 100755 index 00000000..d77ba757 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -0,0 +1,138 @@ +/* + * 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.hateoas; + +import java.io.Serializable; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +import org.springframework.util.Assert; + +/** + * Value object for links. + * + * @author Oliver Gierke + */ +@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE) +public class Link implements Serializable { + + private static final long serialVersionUID = -9037755944661782121L; + + public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"; + + public static final String REL_SELF = "self"; + public static final String REL_FIRST = "first"; + public static final String REL_PREVIOUS = "previous"; + public static final String REL_NEXT = "next"; + public static final String REL_LAST = "last"; + + @XmlAttribute + private String rel; + @XmlAttribute + private String href; + + /** + * Creates a new link to the given URI with the self rel. + * + * @see #REL_SELF + * @param href must not be {@literal null} or empty. + */ + public Link(String href) { + this(href, REL_SELF); + } + + /** + * Creates a new {@link Link} to the given URI with the given rel. + * + * @param href must not be {@literal null} or empty. + * @param rel must not be {@literal null} or empty. + */ + public Link(String href, String rel) { + + Assert.hasText(href, "Href must not be null or empty!"); + Assert.hasText(rel, "Rel must not be null or empty!"); + + this.href = href; + this.rel = rel; + } + + /** + * Empty constructor required by the marshalling framework. + */ + protected Link() { + + } + + /** + * Returns the actual URI the link is pointing to. + * + * @return + */ + public String getHref() { + return href; + } + + /** + * Returns the rel of the link. + * + * @return + */ + public String getRel() { + return rel; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Link)) { + return false; + } + + Link that = (Link) obj; + + return this.href.equals(that.href) && this.rel.equals(that.rel); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + result += 31 * href.hashCode(); + result += 31 * rel.hashCode(); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("{ rel : %s, href : %s }", rel, href); + } +} diff --git a/src/main/java/org/springframework/hateoas/ResourceAssembler.java b/src/main/java/org/springframework/hateoas/ResourceAssembler.java new file mode 100755 index 00000000..132341d2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/ResourceAssembler.java @@ -0,0 +1,32 @@ +/* + * 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.hateoas; + +/** + * Interface for components that convert a domain type into an {@link ResourceSupport}. + * + * @author Oliver Gierke + */ +public interface ResourceAssembler { + + /** + * Converts the given entity into an {@link ResourceSupport}. + * + * @param entity + * @return + */ + D toResource(T entity); +} diff --git a/src/main/java/org/springframework/hateoas/ResourceSupport.java b/src/main/java/org/springframework/hateoas/ResourceSupport.java new file mode 100755 index 00000000..60db627c --- /dev/null +++ b/src/main/java/org/springframework/hateoas/ResourceSupport.java @@ -0,0 +1,130 @@ +/* + * 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.hateoas; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.annotation.XmlElement; + +import org.springframework.util.Assert; + +/** + * Base class for DTOs to collect links. + * + * @author Oliver Gierke + */ +public class ResourceSupport implements Identifiable { + + @XmlElement(name = "link", namespace = Link.ATOM_NAMESPACE) + private List links; + + public ResourceSupport() { + this.links = new ArrayList(); + } + + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ + public Link getId() { + return getLink(Link.REL_SELF); + } + + /** + * Adds the given link to the resource. + * + * @param link + */ + public void add(Link link) { + Assert.notNull(link, "Link must not be null!"); + this.links.add(link); + } + + public void add(Iterable links) { + Assert.notNull(links, "Given links must not be null!"); + for (Link candidate : links) { + add(candidate); + } + } + + public boolean hasLinks() { + return !this.links.isEmpty(); + } + + public boolean hasLink(String rel) { + return getLink(rel) != null; + } + + public List getLinks() { + return Collections.unmodifiableList(links); + } + + /** + * Returns the link with the given rel. + * + * @param rel + * @return the link with the given rel or {@literal null} if none found. + */ + public Link getLink(String rel) { + + for (Link link : links) { + if (link.getRel().equals(rel)) { + return link; + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!obj.getClass().equals(this.getClass())) { + return false; + } + + ResourceSupport that = (ResourceSupport) obj; + + return this.links.equals(that.links); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return this.links.hashCode(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return this.links.toString(); + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java new file mode 100755 index 00000000..61d0f0b4 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -0,0 +1,136 @@ +/* + * 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.hateoas.mvc; + +import java.net.URI; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplate; + +/** + * Builder to ease building {@link Link} instances pointing to Spring MVC controllers. + * + * @author Oliver Gierke + */ +public class ControllerLinkBuilder { + + private final UriComponents builder; + + /** + * Creates a new {@link ControllerLinkBuilder}. + * + * @param builder must not be {@literal null}. + */ + private ControllerLinkBuilder(UriComponentsBuilder builder) { + Assert.notNull(builder); + this.builder = builder.build(); + } + + /** + * Creates a new {@link ControllerLinkBuilder} with a base of the mapping annotated to the given controller class. + * + * @param controller + * @return + */ + public static ControllerLinkBuilder linkTo(Class controller) { + return linkTo(controller, new Object[0]); + } + + public static ControllerLinkBuilder linkTo(Class controller, Object... parameters) { + + RequestMapping annotation = controller.getAnnotation(RequestMapping.class); + String[] mapping = annotation == null ? new String[0] : (String[]) AnnotationUtils.getValue(annotation); + + if (mapping.length > 1) { + throw new IllegalStateException("Multiple controller mappings defined! Unable to build URI!"); + } + + ControllerLinkBuilder builder = new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()); + + if (mapping.length == 0) { + return builder; + } + + UriTemplate template = new UriTemplate(mapping[0]); + return builder.slash(template.expand(parameters)); + } + + /** + * Adds the given object's {@link String} representation as sub-resource to the current URI. + * + * @param object + * @return + */ + public ControllerLinkBuilder slash(Object object) { + + if (object == null) { + return this; + } + + String[] segments = StringUtils.tokenizeToStringArray(object.toString(), "/"); + return new ControllerLinkBuilder(UriComponentsBuilder.fromUri(builder.toUri()).pathSegment(segments)); + } + + /** + * Adds the given {@link AbstractEntity}'s id as sub-resource. Will simply return the current builder if the given + * entity is {@literal null}. + * + * @param identifyable + * @return + */ + public ControllerLinkBuilder slash(Identifiable identifyable) { + + if (identifyable == null) { + return this; + } + + return slash(identifyable.getId()); + } + + /** + * Returns a URI resulting from the builder. + * + * @return + */ + public URI toUri() { + return builder.encode().toUri(); + } + + public Link withRel(String rel) { + return new Link(this.toString(), rel); + } + + public Link withSelfRel() { + return new Link(this.toString()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return toUri().normalize().toASCIIString(); + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ResourceAssemblerSupport.java b/src/main/java/org/springframework/hateoas/mvc/ResourceAssemblerSupport.java new file mode 100755 index 00000000..61ccb912 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/ResourceAssemblerSupport.java @@ -0,0 +1,157 @@ +/* + * 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 coimport javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlSchema; + + +eed 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.hateoas.mvc; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.ResourceAssembler; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.util.Assert; + +/** + * Base class to implement {@link ResourceAssembler}s. Will automate {@link ResourceSupport} instance creation and make + * sure a self-link is always added. + * + * @author Oliver Gierke + */ +public abstract class ResourceAssemblerSupport, D extends ResourceSupport> implements + ResourceAssembler { + + public static class EntityId { + + private final Object id; + + public EntityId(Object id) { + Assert.notNull(id); + this.id = id; + } + + public static EntityId id(Object object) { + return new EntityId(object); + } + + @Override + public String toString() { + return this.id.toString(); + } + } + + private final Class controllerClass; + private final Class resourceType; + + /** + * Creates a new {@link ResourceAssemblerSupport} using the given controller class and resource type. + * + * @param controllerClass must not be {@literal null}. + * @param resourceType must not be {@literal null}. + */ + public ResourceAssemblerSupport(Class controllerClass, Class resourceType) { + + Assert.notNull(controllerClass); + Assert.notNull(resourceType); + + this.controllerClass = controllerClass; + this.resourceType = resourceType; + } + + /** + * Converts all given entities into resources. + * + * @see #toResource(Object) + * @param entities + * @return + */ + public List toResources(Iterable entities) { + + List result = new ArrayList(); + + for (T entity : entities) { + result.add(toResource(entity)); + } + + return result; + } + + /** + * Creates a new resource and adds a self link to it consisting using the {@link Identifiable}'s id. + * + * @param entity must not be {@literal null}. + * @return + */ + protected D createResource(T entity) { + return createResource(entity, new Object[0]); + } + + protected D createResource(T entity, Object... parameters) { + return createResource(entity, EntityId.id(entity.getId()), parameters); + } + + /** + * Creates a new resource with a self link to the given id. + * + * @param entity + * @param id + * @return + */ + protected D createResource(T entity, EntityId id) { + return createResource(entity, id, new Object[0]); + } + + protected D createResource(T entity, EntityId id, Object... parameters) { + + Assert.notNull(entity); + Assert.notNull(id); + + D instance = instantiateResource(entity); + instance.add(linkTo(controllerClass, unwrapIdentifyables(parameters)).slash(id).withSelfRel()); + return instance; + } + + /** + * Extracts the ids of the given values in case they're {@link Identifiable}s. Returns all other objects as they are. + * + * @param values must not be {@literal null}. + * @return + */ + private Object[] unwrapIdentifyables(Object[] values) { + + List result = new ArrayList(values.length); + + for (Object element : Arrays.asList(values)) { + result.add((element instanceof Identifiable) ? ((Identifiable) element).getId() : element); + } + + return result.toArray(); + } + + /** + * Instantiates the resource object. Default implementation will assume a no-arg constructor and use reflection but + * can be overridden to manually set up the object instance initially (e.g. to improve performance if this becomes an + * issue). + * + * @param entity + * @return + */ + protected D instantiateResource(T entity) { + return BeanUtils.instantiateClass(resourceType); + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/mvc/package-info.java b/src/main/java/org/springframework/hateoas/mvc/package-info.java new file mode 100644 index 00000000..25579f55 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/package-info.java @@ -0,0 +1,6 @@ +/** + * Spring MVC helper classes to build {@link org.springframework.hateoas.Link}s and assemble + * {@link org.springframework.hateoas.ResourceSupport} types. + */ +package org.springframework.hateoas.mvc; + diff --git a/src/main/java/org/springframework/hateoas/package-info.java b/src/main/java/org/springframework/hateoas/package-info.java new file mode 100644 index 00000000..836b5fd9 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/package-info.java @@ -0,0 +1,9 @@ +/** + * Value objects to ease creating {@link org.springframework.hateoas.Link}s and link driven representations for REST webservices. + */ +@XmlSchema(xmlns = { @XmlNs(prefix = "atom", namespaceURI = org.springframework.hateoas.Link.ATOM_NAMESPACE) }, elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) +package org.springframework.hateoas; + +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlSchema; + diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java new file mode 100644 index 00000000..4a4d8a5a --- /dev/null +++ b/src/test/java/org/springframework/hateoas/LinkUnitTest.java @@ -0,0 +1,94 @@ +/* + * 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.hateoas; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Link}. + * + * @author Oliver Gierke + */ +public class LinkUnitTest { + + @Test + public void linkWithHrefOnlyBecomesSelfLink() { + Link link = new Link("foo"); + assertThat(link.getRel(), is(Link.REL_SELF)); + } + + @Test + public void createsLinkFromRelAndHref() { + Link link = new Link("foo", Link.REL_SELF); + assertThat(link.getHref(), is("foo")); + assertThat(link.getRel(), is(Link.REL_SELF)); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullHref() { + new Link(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullRel() { + new Link("foo", null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyHref() { + new Link(""); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyRel() { + new Link("foo", ""); + } + + @Test + public void sameRelAndHrefMakeSameLink() { + + Link left = new Link("foo", Link.REL_SELF); + Link right = new Link("foo", Link.REL_SELF); + + TestUtils.assertEqualAndSameHashCode(left, right); + } + + @Test + public void differentRelMakesDifferentLink() { + + Link left = new Link("foo", Link.REL_PREVIOUS); + Link right = new Link("foo", Link.REL_NEXT); + + TestUtils.assertNotEqualAndDifferentHashCode(left, right); + } + + @Test + public void differentHrefMakesDifferentLink() { + + Link left = new Link("foo", Link.REL_SELF); + Link right = new Link("bar", Link.REL_SELF); + + TestUtils.assertNotEqualAndDifferentHashCode(left, right); + } + + @Test + public void differentTypeDoesNotEqual() { + assertThat(new Link("foo"), is(not((Object) new ResourceSupport()))); + } +} diff --git a/src/test/java/org/springframework/hateoas/ResourceSupportUnitTest.java b/src/test/java/org/springframework/hateoas/ResourceSupportUnitTest.java new file mode 100644 index 00000000..cdc48edc --- /dev/null +++ b/src/test/java/org/springframework/hateoas/ResourceSupportUnitTest.java @@ -0,0 +1,137 @@ +/* + * 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.hateoas; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * Unit tests for {@link ResourceSupport}. + * + * @author Oliver Gierke + */ +public class ResourceSupportUnitTest { + + @Test + public void setsUpWithEmptyLinkList() { + + ResourceSupport support = new ResourceSupport(); + assertThat(support.hasLinks(), is(false)); + assertThat(support.hasLink(Link.REL_SELF), is(false)); + assertThat(support.getLinks().isEmpty(), is(true)); + } + + @Test + public void addsLinkCorrectly() { + + Link link = new Link("foo", Link.REL_NEXT); + ResourceSupport support = new ResourceSupport(); + support.add(link); + + assertThat(support.getId(), is(nullValue())); + assertThat(support.hasLinks(), is(true)); + assertThat(support.hasLink(link.getRel()), is(true)); + assertThat(support.getLink(link.getRel()), is(link)); + } + + @Test + public void addsLinksCorrectly() { + + Link first = new Link("foo", Link.REL_PREVIOUS); + Link second = new Link("bar", Link.REL_NEXT); + + ResourceSupport support = new ResourceSupport(); + support.add(Arrays.asList(first, second)); + + assertThat(support.getId(), is(nullValue())); + assertThat(support.hasLinks(), is(true)); + assertThat(support.getLinks(), hasItems(first, second)); + assertThat(support.getLinks().size(), is(2)); + } + + @Test + public void selfLinkBecomesId() { + + Link link = new Link("foo"); + ResourceSupport support = new ResourceSupport(); + support.add(link); + + assertThat(support.getId(), is(link)); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsNullLinkBeingAdded() { + + ResourceSupport support = new ResourceSupport(); + support.add((Link) null); + } + + @Test(expected = IllegalArgumentException.class) + public void preventsNullLinksBeingAdded() { + ResourceSupport support = new ResourceSupport(); + support.add((Iterable) null); + } + + @Test + public void sameLinkListMeansSameResource() { + + ResourceSupport first = new ResourceSupport(); + ResourceSupport second = new ResourceSupport(); + + TestUtils.assertEqualAndSameHashCode(first, second); + + Link link = new Link("foo"); + first.add(link); + second.add(link); + + TestUtils.assertEqualAndSameHashCode(first, second); + } + + @Test + public void differentLinkListsNotEqual() { + + ResourceSupport first = new ResourceSupport(); + ResourceSupport second = new ResourceSupport(); + second.add(new Link("foo")); + + TestUtils.assertNotEqualAndDifferentHashCode(first, second); + } + + @Test + public void subclassNotEquals() { + + ResourceSupport left = new ResourceSupport(); + ResourceSupport right = new ResourceSupport() { + + @Override + public int hashCode() { + return super.hashCode() + 1; + } + + @Override + public String toString() { + return super.toString() + "1"; + } + }; + + TestUtils.assertNotEqualAndDifferentHashCode(left, right); + } + +} diff --git a/src/test/java/org/springframework/hateoas/TestUtils.java b/src/test/java/org/springframework/hateoas/TestUtils.java new file mode 100644 index 00000000..697358ad --- /dev/null +++ b/src/test/java/org/springframework/hateoas/TestUtils.java @@ -0,0 +1,58 @@ +/* + * 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.hateoas; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * + * @author Oliver Gierke + */ +public class TestUtils { + + @Before + public void setUp() { + + HttpServletRequest request = new MockHttpServletRequest(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes); + } + + public static void assertEqualAndSameHashCode(Object left, Object right) { + + assertThat(left, is(right)); + assertThat(right, is(left)); + assertThat(left, is(left)); + assertThat(left.hashCode(), is(right.hashCode())); + assertThat(left.toString(), is(right.toString())); + } + + public static void assertNotEqualAndDifferentHashCode(Object left, Object right) { + + assertThat(left, is(not(right))); + assertThat(right, is(not(left))); + assertThat(left.hashCode(), is(not(right.hashCode()))); + assertThat(left.toString(), is(not(right.toString()))); + } +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java new file mode 100644 index 00000000..ebf6c97d --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java @@ -0,0 +1,129 @@ +/* + * 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.hateoas.mvc; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.TestUtils; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * + * @author Oliver Gierke + */ +public class ControllerLinkBuilderUnitTest extends TestUtils { + + @Test + public void createsLinkToControllerRoot() { + + Link link = linkTo(PersonController.class).withSelfRel(); + assertThat(link.getRel(), is(Link.REL_SELF)); + assertThat(link.getHref(), Matchers.endsWith("/people")); + } + + @Test + public void createsLinkToParameterizedControllerRoot() { + + Link link = linkTo(PersonsAddressesController.class, 15).withSelfRel(); + assertThat(link.getRel(), is(Link.REL_SELF)); + assertThat(link.getHref(), Matchers.endsWith("/people/15/addresses")); + } + + @Test + public void createsLinkToSubResource() { + + Link link = linkTo(PersonController.class).slash("something").withSelfRel(); + assertThat(link.getRel(), is(Link.REL_SELF)); + assertThat(link.getHref(), Matchers.endsWith("/people/something")); + } + + @Test + public void createsLinkWithCustomRel() { + + Link link = linkTo(PersonController.class).withRel(Link.REL_NEXT); + assertThat(link.getRel(), is(Link.REL_NEXT)); + assertThat(link.getHref(), Matchers.endsWith("/people")); + } + + @Test(expected = IllegalStateException.class) + public void rejectsControllerWithMultipleMappings() { + linkTo(InvalidController.class); + } + + @Test + public void createsLinkToUnmappedController() { + + linkTo(UnmappedController.class); + } + + @Test + @SuppressWarnings("unchecked") + public void usesIdOfIdentifyableForPathSegment() { + + Identifiable identifyable = mock(Identifiable.class); + when(identifyable.getId()).thenReturn(10L); + + Link link = linkTo(PersonController.class).slash(identifyable).withSelfRel(); + assertThat(link.getHref(), Matchers.endsWith("/people/10")); + } + + @Test + public void appendingNullIsANoOp() { + + Link link = linkTo(PersonController.class).slash(null).withSelfRel(); + assertThat(link.getHref(), Matchers.endsWith("/people")); + + link = linkTo(PersonController.class).slash((Object) null).withSelfRel(); + assertThat(link.getHref(), Matchers.endsWith("/people")); + } + + class Person implements Identifiable { + + Long id; + + @Override + public Long getId() { + return id; + } + } + + @RequestMapping("/people") + class PersonController { + + } + + @RequestMapping("/people/{id}/addresses") + class PersonsAddressesController { + + } + + @RequestMapping({ "/persons", "/people" }) + class InvalidController { + + } + + class UnmappedController { + + } + +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ResourceAssemblerSupportUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ResourceAssemblerSupportUnitTest.java new file mode 100644 index 00000000..361d33db --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/ResourceAssemblerSupportUnitTest.java @@ -0,0 +1,141 @@ +/* + * 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.hateoas.mvc; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; +import static org.springframework.hateoas.mvc.ResourceAssemblerSupport.EntityId.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.TestUtils; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * + * @author Oliver Gierke + */ +public class ResourceAssemblerSupportUnitTest extends TestUtils { + + PersonResourceAssembler assembler = new PersonResourceAssembler(); + Person person; + + @Override + @Before + public void setUp() { + super.setUp(); + this.person = new Person(); + this.person.id = 10L; + this.person.alternateId = "id"; + } + + @Test + public void createsInstanceWithSelfLinkToController() { + + PersonResource resource = assembler.createResource(person); + Link link = resource.getLink(Link.REL_SELF); + + assertThat(link, is(notNullValue())); + assertThat(resource.getLinks().size(), is(1)); + } + + @Test + public void usesAlternateIdIfGivenExplicitly() { + + PersonResource resource = assembler.createResource(person, id(person.alternateId)); + Link selfLink = resource.getId(); + assertThat(selfLink.getHref(), endsWith("/people/id")); + } + + @Test + public void unwrapsIdentifyablesForParameters() { + + PersonResource resource = new PersonResourceAssembler(ParameterizedController.class).createResource(person, person, + "bar"); + Link selfLink = resource.getId(); + assertThat(selfLink.getHref(), endsWith("/people/10/bar/addresses/10")); + } + + @Test + public void convertsEntitiesToResources() { + + Person first = new Person(); + first.id = 1L; + Person second = new Person(); + second.id = 2L; + + List result = assembler.toResources(Arrays.asList(first, second)); + + ControllerLinkBuilder builder = linkTo(PersonController.class); + + PersonResource firstResource = new PersonResource(); + firstResource.add(builder.slash(1L).withSelfRel()); + + PersonResource secondResource = new PersonResource(); + secondResource.add(builder.slash(1L).withSelfRel()); + + assertThat(result.size(), is(2)); + assertThat(result, hasItems(firstResource, secondResource)); + } + + @RequestMapping("/people") + static class PersonController { + + } + + @RequestMapping("/people/{id}/{foo}/addresses") + static class ParameterizedController { + + } + + static class Person implements Identifiable { + + Long id; + String alternateId; + + @Override + public Long getId() { + return id; + } + } + + static class PersonResource extends ResourceSupport { + + } + + class PersonResourceAssembler extends ResourceAssemblerSupport { + + public PersonResourceAssembler() { + this(PersonController.class); + } + + public PersonResourceAssembler(Class controllerType) { + super(controllerType, PersonResource.class); + } + + @Override + public PersonResource toResource(Person entity) { + return createResource(entity); + } + } +}