diff --git a/src/main/java/org/springframework/hateoas/mvc/TypeReferences.java b/src/main/java/org/springframework/hateoas/mvc/TypeReferences.java new file mode 100644 index 00000000..2ea7afa0 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/TypeReferences.java @@ -0,0 +1,189 @@ +/* + * Copyright 2015 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.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.util.Assert; + +/** + * Helper to easily create {@link ParameterizedTypeReference} instances to Spring HATEOAS resource types. They're + * basically a shortcut over using a verbose {@code new ParameterizedTypeReference>() . + * + * @author Oliver Gierke + * @since 0.17 + */ +public class TypeReferences { + + /** + * A {@link ParameterizedTypeReference} to return a {@link org.springframework.hateoas.Resource} of some type. + * + * @author Oliver Gierke + * @since 0.17 + */ + public static class Resource extends SyntheticParameterizedTypeReference> {} + + /** + * A {@link ParameterizedTypeReference} to return a {@link org.springframework.hateoas.Resources} of some type. + * + * @author Oliver Gierke + * @since 0.17 + */ + public static class Resources extends + SyntheticParameterizedTypeReference> {} + + /** + * A {@link ParameterizedTypeReference} to return a {@link org.springframework.hateoas.PagedResources} of some type. + * + * @author Oliver Gierke + * @since 0.17 + */ + public static class PagedResources extends + SyntheticParameterizedTypeReference> {} + + /** + * Special {@link ParameterizedTypeReference} to customize the generic type detection and eventually return a sythetic + * {@link ParameterizedType} to represent the resource type along side its generic parameter. + * + * @author Oliver Gierke + * @since 0.17 + */ + private static abstract class SyntheticParameterizedTypeReference extends ParameterizedTypeReference { + + private final Type type; + + @SuppressWarnings("rawtypes") + protected SyntheticParameterizedTypeReference() { + + Class foo = getClass(); + Type genericSuperclass = foo.getGenericSuperclass(); + ParameterizedType bar = (ParameterizedType) genericSuperclass; + Type domainType = bar.getActualTypeArguments()[0]; + + Class parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(getClass()); + Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass(); + Assert.isInstanceOf(ParameterizedType.class, type); + ParameterizedType parameterizedType = (ParameterizedType) type; + Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1); + + Class resourceType = GenericTypeResolver.resolveType(parameterizedType.getActualTypeArguments()[0], + new HashMap()); + + this.type = new SyntheticParameterizedType(resourceType, domainType); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.ParameterizedTypeReference#getType() + */ + public Type getType() { + return this.type; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.ParameterizedTypeReference#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + return (this == obj || (obj instanceof SyntheticParameterizedTypeReference && this.type + .equals(((SyntheticParameterizedTypeReference) obj).type))); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.ParameterizedTypeReference#hashCode() + */ + @Override + public int hashCode() { + return this.type.hashCode(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.ParameterizedTypeReference#toString() + */ + @Override + public String toString() { + return "SyntheticParameterizedTypeReference<" + this.type + ">"; + } + + private static Class findParameterizedTypeReferenceSubclass(Class child) { + + Class parent = child.getSuperclass(); + if (Object.class.equals(parent)) { + throw new IllegalStateException("Expected SyntheticParameterizedTypeReference superclass"); + } else if (SyntheticParameterizedTypeReference.class.equals(parent)) { + return child; + } else { + return findParameterizedTypeReferenceSubclass(parent); + } + } + } + + /** + * A sythetic {@link ParameterizedType}. + * + * @author Oliver Gierke + * @since 0.17 + */ + private static final class SyntheticParameterizedType implements ParameterizedType, Serializable { + + private static final long serialVersionUID = -521679299810654826L; + + private final Type rawType; + private final Type[] typeArguments; + + public SyntheticParameterizedType(Type rawType, Type... typeArguments) { + + this.rawType = rawType; + this.typeArguments = typeArguments; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getActualTypeArguments() + */ + @Override + public Type[] getActualTypeArguments() { + return this.typeArguments; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getRawType() + */ + @Override + public Type getRawType() { + return this.rawType; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.ParameterizedType#getOwnerType() + */ + @Override + public Type getOwnerType() { + return null; + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mvc/TypeReferencesIntegrationTests.java b/src/test/java/org/springframework/hateoas/mvc/TypeReferencesIntegrationTests.java new file mode 100644 index 00000000..19c8ebf6 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/TypeReferencesIntegrationTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 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.springframework.test.web.client.MockRestServiceServer.*; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.*; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +/** + * Integration tests for {@link TypeReferences}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class TypeReferencesIntegrationTests { + + private static final String USER = "\"firstname\" : \"Dave\", \"lastname\" : \"Matthews\""; + private static final String RESOURCE = String.format("{ \"_links\" : { \"self\" : \"/resource\" }, %s }", USER); + private static final String RESOURCES_OF_USER = String.format( + "{ \"_links\" : { \"self\" : \"/resources\" }, \"_embedded\" : { \"users\" : [ { %s } ] }}", USER); + private static final String RESOURCES_OF_RESOURCE = String.format( + "{ \"_links\" : { \"self\" : \"/resources\" }, \"_embedded\" : { \"users\" : [ %s ] }}", RESOURCE); + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class Config { + + public @Bean RestTemplate template() { + return new RestTemplate(); + } + } + + @Autowired RestTemplate template; + MockRestServiceServer server; + + @Before + public void setUp() { + this.server = createServer(template); + } + + /** + * @see #306 + */ + @Test + public void usesResourceTypeReference() { + + server.expect(requestTo("/resource")).andRespond(withSuccess(RESOURCE, MediaTypes.HAL_JSON)); + + ResponseEntity> response = template.exchange("/resource", HttpMethod.GET, null, + new TypeReferences.Resource() {}); + + assertExpectedUserResource(response.getBody()); + } + + /** + * @see #306 + */ + @Test + public void usesResourcesTypeReference() { + + server.expect(requestTo("/resources")).andRespond(withSuccess(RESOURCES_OF_USER, MediaTypes.HAL_JSON)); + + ResponseEntity> response = template.exchange("/resources", HttpMethod.GET, null, + new TypeReferences.Resources() {}); + Resources body = response.getBody(); + + assertThat(body.hasLink("self"), is(true)); + + Collection nested = body.getContent(); + + assertThat(nested, hasSize(1)); + assertExpectedUser(nested.iterator().next()); + } + + /** + * @see #306 + */ + @Test + public void usesResourcesOfResourceTypeReference() { + + server.expect(requestTo("/resources")).andRespond(withSuccess(RESOURCES_OF_RESOURCE, MediaTypes.HAL_JSON)); + + ResponseEntity>> response = template.exchange("/resources", HttpMethod.GET, null, + new TypeReferences.Resources>() {}); + Resources> body = response.getBody(); + + assertThat(body.hasLink("self"), is(true)); + + Collection> nested = body.getContent(); + + assertThat(nested, hasSize(1)); + assertExpectedUserResource(nested.iterator().next()); + } + + private static void assertExpectedUserResource(Resource user) { + + assertThat(user.hasLink("self"), is(true)); + assertExpectedUser(user.getContent()); + } + + private static void assertExpectedUser(User user) { + + assertThat(user.firstname, is("Dave")); + assertThat(user.lastname, is("Matthews")); + } + + static class User { + public String firstname, lastname; + } +}