#135 - HAL embedded resources are now rendered as collection by default.

To get consistent representations rendered for single-item collections and multi-item collections, we now use an enforceEmbeddedCollections flag in the Jackson 2 HAL module. Adapted HalEmbeddedBuilder to respect that setting and exposed it via the HandlerInstantiator for user level customization.

Changed RelProvider.getSingleResourceRel(…) to getItemResourceRel(…).
This commit is contained in:
Oliver Gierke
2014-01-24 14:50:41 +01:00
parent 9caa352470
commit bb0a578701
13 changed files with 77 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -18,11 +18,25 @@ package org.springframework.hateoas;
import org.springframework.plugin.core.Plugin;
/**
* API to provide relation types for collections and items of the given type.
*
* @author Oliver Gierke
*/
public interface RelProvider extends Plugin<Class<?>> {
String getSingleResourceRelFor(Class<?> type);
/**
* Returns the relation type to be used to point to an item resource of the given type.
*
* @param type must not be {@literal null}.
* @return
*/
String getItemResourceRelFor(Class<?> type);
/**
* Returns the relation type to be used to point to a collection resource of the given type.
*
* @param type must not be {@literal null}.
* @return
*/
String getCollectionResourceRelFor(Class<?> type);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -52,7 +52,7 @@ public class AnnotationRelProvider implements RelProvider {
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Object)
*/
@Override
public String getSingleResourceRelFor(Class<?> type) {
public String getItemResourceRelFor(Class<?> type) {
Relation annotation = lookupAnnotation(type);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -52,7 +52,7 @@ public class DefaultRelProvider implements RelProvider {
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getSingleResourceRelFor(Class<?> type) {
public String getItemResourceRelFor(Class<?> type) {
return StringUtils.uncapitalize(type.getSimpleName());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -37,8 +37,8 @@ public class DelegatingRelProvider implements RelProvider {
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getSingleResourceRelFor(Class<?> type) {
return providers.getPluginFor(type).getSingleResourceRelFor(type);
public String getItemResourceRelFor(Class<?> type) {
return providers.getPluginFor(type).getItemResourceRelFor(type);
}
/*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -33,6 +33,6 @@ public class EvoInflectorRelProvider extends DefaultRelProvider {
*/
@Override
public String getCollectionResourceRelFor(Class<?> type) {
return English.plural(getSingleResourceRelFor(type));
return English.plural(getItemResourceRelFor(type));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -38,14 +38,16 @@ class HalEmbeddedBuilder {
private final Map<String, List<Object>> embeddeds = new HashMap<String, List<Object>>();
private final RelProvider provider;
private final boolean enforceCollections;
/**
* Creates a new {@link HalEmbeddedBuilder} using the given {@link RelProvider}.
*
* @param provider can be {@literal null}.
*/
public HalEmbeddedBuilder(RelProvider provider) {
public HalEmbeddedBuilder(RelProvider provider, boolean enforceCollections) {
this.provider = provider;
this.enforceCollections = enforceCollections;
}
/**
@@ -65,7 +67,7 @@ class HalEmbeddedBuilder {
String rel = getDefaultedRelFor(type, true);
if (!embeddeds.containsKey(rel)) {
rel = getDefaultedRelFor(type, false);
rel = getDefaultedRelFor(type, enforceCollections);
}
List<Object> currentValue = embeddeds.get(rel);
@@ -89,7 +91,7 @@ class HalEmbeddedBuilder {
return DEFAULT_REL;
}
String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getSingleResourceRelFor(type);
String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type);
return rel == null ? DEFAULT_REL : rel;
}

View File

@@ -194,7 +194,7 @@ public class Jackson1HalModule extends SimpleModule {
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider);
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, false);
for (Object resource : value) {
builder.add(resource);

View File

@@ -238,17 +238,19 @@ public class Jackson2HalModule extends SimpleModule {
private final BeanProperty property;
private final RelProvider relProvider;
private final boolean enforceEmbeddedCollections;
public HalResourcesSerializer(RelProvider relPorvider) {
this(null, relPorvider);
public HalResourcesSerializer(RelProvider relPorvider, boolean enforceEmbeddedCollections) {
this(null, relPorvider, enforceEmbeddedCollections);
}
public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) {
public HalResourcesSerializer(BeanProperty property, RelProvider relProvider, boolean enforceEmbeddedCollections) {
super(Collection.class, false);
this.property = property;
this.relProvider = relProvider;
this.enforceEmbeddedCollections = enforceEmbeddedCollections;
}
/*
@@ -261,7 +263,7 @@ public class Jackson2HalModule extends SimpleModule {
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider);
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, enforceEmbeddedCollections);
for (Object resource : value) {
builder.add(resource);
@@ -272,8 +274,11 @@ public class Jackson2HalModule extends SimpleModule {
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Resource.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
JsonSerializer<Object> valueSerializer = enforceEmbeddedCollections ? provider.findValueSerializer(valueType,
property) : new OptionalListJackson2Serializer(property);
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null);
provider.findKeySerializer(keyType, null), valueSerializer, null);
serializer.serialize(builder.asMap(), jgen, provider);
}
@@ -281,12 +286,11 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalResourcesSerializer(property, relProvider);
return new HalResourcesSerializer(property, relProvider, enforceEmbeddedCollections);
}
@Override
public JavaType getContentType() {
// TODO Auto-generated method stub
return null;
}
@@ -601,9 +605,14 @@ public class Jackson2HalModule extends SimpleModule {
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
this(resolver, curieProvider, true);
}
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, boolean enforceEmbeddedCollections) {
Assert.notNull(resolver, "RelProvider must not be null!");
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver));
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver,
enforceEmbeddedCollections));
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -56,8 +56,8 @@ public class ControllerRelProvider implements RelProvider {
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getSingleResourceRelFor(Class<?> resource) {
return providers.getPluginFor(entityType).getSingleResourceRelFor(resource);
public String getItemResourceRelFor(Class<?> resource) {
return providers.getPluginFor(entityType).getItemResourceRelFor(resource);
}
/*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -27,6 +27,8 @@ import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
/**
* Unit tests for {@link DelegatingRelProvider}.
*
* @author Oliver Gierke
*/
public class DelegatingRelProviderUnitTest {
@@ -40,11 +42,11 @@ public class DelegatingRelProviderUnitTest {
RelProvider delegatingProvider = new DelegatingRelProvider(registry);
assertThat(delegatingProvider.supports(Sample.class), is(true));
assertThat(delegatingProvider.getSingleResourceRelFor(Sample.class), is("foo"));
assertThat(delegatingProvider.getItemResourceRelFor(Sample.class), is("foo"));
assertThat(delegatingProvider.getCollectionResourceRelFor(Sample.class), is("bar"));
assertThat(delegatingProvider.supports(String.class), is(true));
assertThat(delegatingProvider.getSingleResourceRelFor(String.class), is("string"));
assertThat(delegatingProvider.getItemResourceRelFor(String.class), is("string"));
assertThat(delegatingProvider.getCollectionResourceRelFor(String.class), is("stringList"));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -37,7 +37,7 @@ public class EvoInflectorRelProviderUnitTest {
}
private void assertRels(Class<?> type, String singleRel, String collectionRel) {
assertThat(provider.getSingleResourceRelFor(type), is(singleRel));
assertThat(provider.getItemResourceRelFor(type), is(singleRel));
assertThat(provider.getCollectionResourceRelFor(type), is(collectionRel));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -84,15 +84,28 @@ public class HalEmbeddedBuilderUnitTest {
public void addsNoEmbeddedsForResourceWithoutContent() {
Resource<?> resource = BeanUtils.instantiateClass(Resource.class);
HalEmbeddedBuilder halEmbeddedBuilder = new HalEmbeddedBuilder(provider);
HalEmbeddedBuilder halEmbeddedBuilder = new HalEmbeddedBuilder(provider, true);
halEmbeddedBuilder.add(resource);
assertThat(halEmbeddedBuilder.asMap().isEmpty(), is(true));
}
/**
* @see #135
*/
@Test
public void forcesCollectionRelToBeUsedIfConfigured() {
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, true);
builder.add("Sample");
assertThat(builder.asMap().get("string"), is(nullValue()));
assertThat(builder.asMap().get("strings"), hasItem("Sample"));
}
private Map<String, List<Object>> setUpBuilder(Object... values) {
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider);
HalEmbeddedBuilder builder = new HalEmbeddedBuilder(provider, false);
for (Object value : values) {
builder.add(value);

View File

@@ -52,10 +52,10 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}";
static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
static final String ANNOTATED_EMBEDDED_RESOURCES_REFERENCE = "{\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
static final String ANNOTATED_PAGED_RESOURCES = "{\"_links\":{\"next\":{\"href\":\"foo\"},\"prev\":{\"href\":\"bar\"}},\"_embedded\":{\"pojos\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]},\"page\":{\"size\":2,\"totalElements\":4,\"totalPages\":2,\"number\":0}}";