#126, #123 - Added support for HAL curies.

Introduced CurieProvider interface as well as a DefaultCurieProvider that can be registered as Spring Bean to enable Links being rendered in a curie namespace and an additional curie link exposing the metadata of how to lookup the rel definition.

Deprecated Jackson 1 integration. Added helper class to find out whether a relation type is defined by the IANA.
This commit is contained in:
Oliver Gierke
2013-12-17 11:23:30 +01:00
parent fac6bbb2cc
commit 9eacd07203
11 changed files with 383 additions and 36 deletions

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2013 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
/**
* Static class to find out whether a relation type is defined by the IANA.
*
* @see http://www.iana.org/assignments/link-relations/link-relations.xhtml
* @author Oliver Gierke
*/
public class IanaRels {
private static final Collection<String> RELS;
static {
Collection<String> rels = new HashSet<String>();
rels.addAll(Arrays.asList("about", "alternate", "appendix", "archives", "author", "bookmark", "canonical",
"chapter", "collection", "contents", "copyright", "create-form", "current", "describedby", "describes",
"disclosure", "duplicate", "edit", "edit-form", "edit-media", "enclosure", "first", "glossary", "help",
"hosts", "hub", "icon", "index", "item", "last", "latest-version", "license", "lrdd", "memento", "monitor",
"monitor-group", "next", "next-archive", "nofollow", "noreferrer", "original", "payment",
"predecessor-version", "prefetch", "prev", "preview", "previous", "prev-archive", "privacy-policy", "profile",
"related", "replies", "search", "section", "self", "service", "start", "stylesheet", "subsection",
"successor-version", "tag", "terms-of-service", "timegate", "timemap", "type", "up", "version-history", "via",
"working-copy", "working-copy-of"));
RELS = Collections.unmodifiableCollection(rels);
}
/**
* Returns whether the given relation type is defined by the IANA.
*
* @param rel the relation type to check
* @return
*/
public static boolean isIanaRel(String rel) {
return rel == null ? false : RELS.contains(rel);
}
}

View File

@@ -23,6 +23,7 @@ import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -42,6 +43,7 @@ import org.springframework.hateoas.core.DefaultLinkDiscoverer;
import org.springframework.hateoas.core.DefaultRelProvider;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
import org.springframework.hateoas.hal.Jackson1HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule;
@@ -63,6 +65,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*
* @author Oliver Gierke
*/
@SuppressWarnings("deprecation")
class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer";
@@ -172,7 +175,8 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
private BeanFactory factory;
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
@@ -226,7 +230,16 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
ObjectMapper mapper = (ObjectMapper) objectMapper;
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider));
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider, getCurieProvider()));
}
private CurieProvider getCurieProvider() {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
}
@@ -236,6 +249,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
*
* @author Oliver Gierke
*/
@Deprecated
private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
private BeanFactory beanFactory;

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013 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.hal;
import org.springframework.hateoas.Link;
/**
* API to provide HAL curie information for links.
*
* @see http://tools.ietf.org/html/draft-kelly-json-hal#section-8.2
* @author Oliver Gierke
*/
public interface CurieProvider {
/**
* Returns the rel to be rendered for the given {@link Link}. Will potentially prefix the rel but also might decide
* not to, depending on the actual rel.
*
* @param link
* @return
*/
String getNamespacedRelFrom(Link link);
/**
* Returns an object to render as the base curie information. Implementations have to make sure, the retunred
* instances renders as defined in the spec.
*
* @return must not be {@literal null}.
*/
Object getCurieInformation();
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2013 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.hal;
import org.springframework.hateoas.IanaRels;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
import org.springframework.web.util.UriTemplate;
/**
* @author Oliver Gierke
*/
public class DefaultCurieProvider implements CurieProvider {
private final Curie curie;
/**
* Creates a new {@link DefaultCurieProvider} for the given name and {@link UriTemplate}.
*
* @param name must not be {@literal null} or empty.
* @param uriTemplate must not be {@literal null} and contain exactly one template variable.
*/
public DefaultCurieProvider(String name, UriTemplate uriTemplate) {
Assert.hasText(name, "Name must not be null or empty!");
Assert.notNull(uriTemplate, "UriTemplate must not be null!");
Assert.isTrue(uriTemplate.getVariableNames().size() == 1,
String.format("Expected a single template variable in the UriTemplate %s!", uriTemplate.toString()));
this.curie = new Curie(name, uriTemplate.toString());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getCurieInformation()
*/
@Override
public Curie getCurieInformation() {
return curie;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(org.springframework.hateoas.Link)
*/
@Override
public String getNamespacedRelFrom(Link link) {
String rel = link.getRel();
boolean prefixingNeeded = !IanaRels.isIanaRel(rel) && !rel.contains(":");
return prefixingNeeded ? String.format("%s:%s", curie.name, rel) : rel;
}
/**
* Value object to get the curie {@link Link} rendered in JSON.
*
* @author Oliver Gierke
*/
@SuppressWarnings("unused")
private static class Curie extends Link {
private static final long serialVersionUID = 1L;
private final String name;
private final boolean templated = true;
public Curie(String name, String href) {
super(href, "curies");
this.name = name;
}
public String getName() {
return name;
}
public boolean isTemplated() {
return templated;
}
}
}

View File

@@ -66,9 +66,11 @@ import org.springframework.util.StringUtils;
/**
* Jackson 1 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON.
*
* @deprecated use Jackson 2 instead
* @author Alexander Baetz
* @author Oliver Gierke
*/
@Deprecated
public class Jackson1HalModule extends SimpleModule {
/**

View File

@@ -17,6 +17,7 @@ package org.springframework.hateoas.hal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
@@ -91,15 +92,17 @@ public class Jackson2HalModule extends SimpleModule {
public static class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
private final BeanProperty property;
private final CurieProvider curieProvider;
public HalLinkListSerializer() {
this(null);
public HalLinkListSerializer(CurieProvider curieProvider) {
this(null, curieProvider);
}
public HalLinkListSerializer(BeanProperty property) {
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider) {
super(List.class, false);
this.property = property;
this.curieProvider = curieProvider;
}
/*
@@ -113,17 +116,28 @@ public class Jackson2HalModule extends SimpleModule {
JsonGenerationException {
// sort links according to their relation
Map<String, List<Link>> sortedLinks = new LinkedHashMap<String, List<Link>>();
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();
boolean prefixingRequired = curieProvider != null;
for (Link link : value) {
if (sortedLinks.get(link.getRel()) == null) {
sortedLinks.put(link.getRel(), new ArrayList<Link>());
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<Object>());
}
sortedLinks.get(link.getRel()).add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired) {
Object curieInformation = curieProvider.getCurieInformation();
sortedLinks.put("curies", Arrays.asList(curieInformation));
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Link.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
@@ -141,7 +155,7 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new HalLinkListSerializer(property);
return new HalLinkListSerializer(property, curieProvider);
}
/*
@@ -294,7 +308,7 @@ public class Jackson2HalModule extends SimpleModule {
ContextualSerializer {
private final BeanProperty property;
private JsonSerializer<Object> serializer;
private final Map<Class<?>, JsonSerializer<Object>> serializers;
public OptionalListJackson2Serializer() {
this(null);
@@ -309,13 +323,12 @@ public class Jackson2HalModule extends SimpleModule {
super(List.class, false);
this.property = property;
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
}
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.
* TypeSerializer)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
@@ -324,9 +337,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator,
* com.fasterxml.jackson.databind.SerializerProvider)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
@@ -334,6 +345,10 @@ public class Jackson2HalModule extends SimpleModule {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if (list.size() == 1) {
serializeContents(list.iterator(), jgen, provider);
return;
@@ -352,27 +367,35 @@ public class Jackson2HalModule extends SimpleModule {
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
if (serializer == null) {
serializer = provider.findValueSerializer(elem.getClass(), property);
}
serializer.serialize(elem, jgen, provider);
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
public JsonSerializer<?> getContentSerializer() {
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
@@ -561,10 +584,11 @@ public class Jackson2HalModule extends SimpleModule {
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
public HalHandlerInstantiator(RelProvider resolver) {
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
Assert.notNull(resolver, "RelProvider must not be null!");
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(null, resolver));
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver));
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider));
}
private Object findInstance(Class<?> type) {