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:
59
src/main/java/org/springframework/hateoas/IanaRels.java
Normal file
59
src/main/java/org/springframework/hateoas/IanaRels.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user