/* * Copyright 2012-2016 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 java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.NoSuchMessageException; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.KeyDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase; import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.ContainerSerializer; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.fasterxml.jackson.databind.ser.std.MapSerializer; import com.fasterxml.jackson.databind.ser.std.NonTypedScalarSerializerBase; import com.fasterxml.jackson.databind.type.TypeFactory; /** * Jackson 2 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON. * * @author Alexander Baetz * @author Oliver Gierke */ public class Jackson2HalModule extends SimpleModule { private static final long serialVersionUID = 7806951456457932384L; private static final Link CURIES_REQUIRED_DUE_TO_EMBEDS = new Link("__rel__", "¯\\_(ツ)_/¯"); public Jackson2HalModule() { super("json-hal-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas")); setMixInAnnotation(Link.class, LinkMixin.class); setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class); setMixInAnnotation(Resources.class, ResourcesMixin.class); } /** * Returns whether the module was already registered in the given {@link ObjectMapper}. * * @param mapper must not be {@literal null}. * @return */ public static boolean isAlreadyRegisteredIn(ObjectMapper mapper) { Assert.notNull(mapper, "ObjectMapper must not be null!"); return LinkMixin.class.equals(mapper.findMixInClassFor(Link.class)); } /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. * * @author Alexander Baetz * @author Oliver Gierke */ public static class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer { private static final long serialVersionUID = -1844788111509966406L; private static final String RELATION_MESSAGE_TEMPLATE = "_links.%s.title"; private final BeanProperty property; private final CurieProvider curieProvider; private final EmbeddedMapper mapper; private final MessageSourceAccessor accessor; public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor) { this(null, curieProvider, mapper, accessor); } public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor) { super(TypeFactory.defaultInstance().constructType(List.class)); this.property = property; this.curieProvider = curieProvider; this.mapper = mapper; this.accessor = accessor; } /* * (non-Javadoc) * @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(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { // sort links according to their relation Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>(); List<Link> links = new ArrayList<Link>(); boolean prefixingRequired = curieProvider != null; boolean curiedLinkPresent = false; boolean skipCuries = !jgen.getOutputContext().getParent().inRoot(); Object currentValue = jgen.getCurrentValue(); if (currentValue instanceof Resources) { if (mapper.hasCuriedEmbed((Resources<?>) currentValue)) { curiedLinkPresent = true; } } for (Link link : value) { if (link.equals(CURIES_REQUIRED_DUE_TO_EMBEDS)) { continue; } String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel(); if (!link.getRel().equals(rel)) { curiedLinkPresent = true; } if (sortedLinks.get(rel) == null) { sortedLinks.put(rel, new ArrayList<Object>()); } links.add(link); sortedLinks.get(rel).add(toHalLink(link)); } if (!skipCuries && prefixingRequired && curiedLinkPresent) { ArrayList<Object> curies = new ArrayList<Object>(); curies.add(curieProvider.getCurieInformation(new Links(links))); sortedLinks.put("curies", curies); } TypeFactory typeFactory = provider.getConfig().getTypeFactory(); JavaType keyType = typeFactory.uncheckedSimpleType(String.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, provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null); serializer.serialize(sortedLinks, jgen, provider); } /** * Wraps the given link into a HAL specific extension. * * @param link must not be {@literal null}. * @return */ private HalLink toHalLink(Link link) { String rel = link.getRel(); String title = getTitle(rel); if (title == null) { title = getTitle(rel.contains(":") ? rel.substring(rel.indexOf(":") + 1) : rel); } return new HalLink(link, title); } /** * Returns the title for the given local link relation resolved through the configured {@link MessageSourceAccessor} * . * * @param localRel must not be {@literal null} or empty. * @return */ private String getTitle(String localRel) { Assert.hasText(localRel, "Local relation must not be null or empty!"); try { return accessor == null ? null : accessor.getMessage(String.format(RELATION_MESSAGE_TEMPLATE, localRel)); } catch (NoSuchMessageException o_O) { return null; } } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty) */ @Override public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { return new HalLinkListSerializer(property, curieProvider, mapper, accessor); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override public JavaType getContentType() { return null; } /* * (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#isEmpty(java.lang.Object) */ public boolean isEmpty(List<Link> value) { return isEmpty(null, value); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ public boolean isEmpty(SerializerProvider provider, List<Link> value) { return value.isEmpty(); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override public boolean hasSingleElement(List<Link> value) { return value.size() == 1; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) */ @Override protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) { return null; } } /** * Custom {@link JsonSerializer} to render {@link Resource}-Lists in HAL compatible JSON. Renders the list as a Map. * * @author Alexander Baetz * @author Oliver Gierke */ public static class HalResourcesSerializer extends ContainerSerializer<Collection<?>> implements ContextualSerializer { private static final long serialVersionUID = 8030706944344625390L; private final BeanProperty property; private final EmbeddedMapper embeddedMapper; public HalResourcesSerializer(EmbeddedMapper embeddedMapper) { this(null, embeddedMapper); } public HalResourcesSerializer(BeanProperty property, EmbeddedMapper embeddedMapper) { super(TypeFactory.defaultInstance().constructType(Collection.class)); this.property = property; this.embeddedMapper = embeddedMapper; } /* * (non-Javadoc) * * @see org.codehaus.jackson.map.ser.std.SerializerBase#serialize(java.lang.Object, org.codehaus.jackson.JsonGenerator, * org.codehaus.jackson.map.SerializerProvider) */ @Override public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { Map<String, Object> embeddeds = embeddedMapper.map(value); Object currentValue = jgen.getCurrentValue(); if (currentValue instanceof ResourceSupport) { if (embeddedMapper.hasCuriedEmbed(value)) { ((ResourceSupport) currentValue).add(CURIES_REQUIRED_DUE_TO_EMBEDS); } } provider.findValueSerializer(Map.class, property).serialize(embeddeds, jgen, provider); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { return new HalResourcesSerializer(property, embeddedMapper); } @Override public JavaType getContentType() { return null; } @Override public JsonSerializer<?> getContentSerializer() { return null; } public boolean isEmpty(Collection<?> value) { return isEmpty(null, value); } public boolean isEmpty(SerializerProvider provider, Collection<?> value) { return value.isEmpty(); } @Override public boolean hasSingleElement(Collection<?> value) { return value.size() == 1; } @Override protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) { return null; } } /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the {@link Link} as * immediate object if we have a single one or as array if we have multiple ones. * * @author Alexander Baetz * @author Oliver Gierke */ public static class OptionalListJackson2Serializer extends ContainerSerializer<Object> implements ContextualSerializer { private static final long serialVersionUID = 3700806118177419817L; private final BeanProperty property; private final Map<Class<?>, JsonSerializer<Object>> serializers; public OptionalListJackson2Serializer() { this(null); } /** * Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}. * * @param property */ public OptionalListJackson2Serializer(BeanProperty property) { super(TypeFactory.defaultInstance().constructType(List.class)); 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) */ @Override public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) { throw new UnsupportedOperationException("not implemented"); } /* * (non-Javadoc) * @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, JsonGenerationException { List<?> list = (List<?>) value; if (list.isEmpty()) { return; } if (list.size() == 1) { serializeContents(list.iterator(), jgen, provider); return; } jgen.writeStartArray(); serializeContents(list.iterator(), jgen, provider); jgen.writeEndArray(); } private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { while (value.hasNext()) { Object elem = value.next(); if (elem == null) { provider.defaultSerializeNull(jgen); } else { getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider); } } } 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 public JavaType getContentType() { return null; } /* * (non-Javadoc) * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override public boolean hasSingleElement(Object arg0) { return false; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ public boolean isEmpty(Object value) { return isEmpty(null, value); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ public boolean isEmpty(SerializerProvider provider, Object value) { return false; } /* * (non-Javadoc) * * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, * com.fasterxml.jackson.databind.BeanProperty) */ @Override public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { return new OptionalListJackson2Serializer(property); } } public static class HalLinkListDeserializer extends ContainerDeserializerBase<List<Link>> { private static final long serialVersionUID = 6420432361123210955L; public HalLinkListDeserializer() { super(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, Link.class)); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override public JavaType getContentType() { return null; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override public JsonDeserializer<Object> getContentDeserializer() { return null; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) */ @Override public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { List<Link> result = new ArrayList<Link>(); String relation; Link link; // links is an object, so we parse till we find its end. while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { throw new JsonParseException("Expected relation name", jp.getCurrentLocation()); } // save the relation in case the link does not contain it relation = jp.getText(); if (JsonToken.START_ARRAY.equals(jp.nextToken())) { while (!JsonToken.END_ARRAY.equals(jp.nextToken())) { link = jp.readValueAs(Link.class); result.add(new Link(link.getHref(), relation)); } } else { link = jp.readValueAs(Link.class); result.add(new Link(link.getHref(), relation)); } } return result; } } public static class HalResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements ContextualDeserializer { private static final long serialVersionUID = 4755806754621032622L; private JavaType contentType; public HalResourcesDeserializer() { this(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, Object.class), null); } public HalResourcesDeserializer(JavaType vc) { this(null, vc); } private HalResourcesDeserializer(JavaType type, JavaType contentType) { super(type); this.contentType = contentType; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType() */ @Override public JavaType getContentType() { return null; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() */ @Override public JsonDeserializer<Object> getContentDeserializer() { return null; } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) */ @Override public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { List<Object> result = new ArrayList<Object>(); JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(contentType); Object object; // links is an object, so we parse till we find its end. while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { throw new JsonParseException("Expected relation name", jp.getCurrentLocation()); } if (JsonToken.START_ARRAY.equals(jp.nextToken())) { while (!JsonToken.END_ARRAY.equals(jp.nextToken())) { object = deser.deserialize(jp, ctxt); result.add(object); } } else { object = deser.deserialize(jp, ctxt); result.add(object); } } return result; } @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { JavaType vc = property.getType().getContentType(); HalResourcesDeserializer des = new HalResourcesDeserializer(vc); return des; } } /** * HandlerInstantiator to create HAL-specific serializers, deserializers etc. * * @author Oliver Gierke */ public static class HalHandlerInstantiator extends HandlerInstantiator { private final Map<Class<?>, Object> serializers = new HashMap<Class<?>, Object>(); private final AutowireCapableBeanFactory delegate; /** * Creates a new {@link HalHandlerInstantiator} using the given {@link RelProvider}, {@link CurieProvider} and * {@link MessageSourceAccessor} and {@link AutowireCapableBeanFactory}. Registers a prepared * {@link HalResourcesSerializer} and {@link HalLinkListSerializer} falling back to instantiation using the given * {@link AutowireCapableBeanFactory} if provided, or simple default constructor instantiation if not. * * @param provider must not be {@literal null}. * @param curieProvider can be {@literal null}. * @param accessor can be {@literal null}. * @param beanFactory can be {@literal null} */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, AutowireCapableBeanFactory beanFactory) { this(provider, curieProvider, accessor, true, beanFactory); } /** * Creates a new {@link HalHandlerInstantiator} using the given {@link RelProvider}, {@link CurieProvider} and * {@link MessageSourceAccessor}. Registers a prepared {@link HalResourcesSerializer} and * {@link HalLinkListSerializer} falling back to instantiation expecting a default constructor. * * @param provider must not be {@literal null}. * @param curieProvider can be {@literal null}. * @param messageSourceAccessor can be {@literal null}. */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor) { this(provider, curieProvider, messageSourceAccessor, true); } /** * Creates a new {@link HalHandlerInstantiator} using the given {@link RelProvider}, {@link CurieProvider} and * {@link MessageSourceAccessor} and whether to enforce embedded collections. Registers a prepared * {@link HalResourcesSerializer} and {@link HalLinkListSerializer} falling back to instantiation expecting a * default constructor. * * @param provider must not be {@literal null}. * @param curieProvider can be {@literal null} * @param accessor can be {@literal null}. * @param enforceEmbeddedCollections */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, boolean enforceEmbeddedCollections) { this(provider, curieProvider, accessor, enforceEmbeddedCollections, null); } private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate) { Assert.notNull(provider, "RelProvider must not be null!"); EmbeddedMapper mapper = new EmbeddedMapper(provider, curieProvider, enforceEmbeddedCollections); this.delegate = delegate; this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper)); this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor)); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) */ @Override public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) { return (JsonDeserializer<?>) findInstance(deserClass); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) */ @Override public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) { return (KeyDeserializer) findInstance(keyDeserClass); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) */ @Override public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) { return (JsonSerializer<?>) findInstance(serClass); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) */ @Override public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) { return (TypeResolverBuilder<?>) findInstance(builderClass); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) */ @Override public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) { return (TypeIdResolver) findInstance(resolverClass); } private Object findInstance(Class<?> type) { Object result = serializers.get(type); return result != null ? result : delegate != null ? delegate.createBean(type) : BeanUtils.instantiateClass(type); } } /** * {@link JsonSerializer} to only render {@link Boolean} values if they're set to {@literal true}. * * @author Oliver Gierke * @since 0.9 */ public static class TrueOnlyBooleanSerializer extends NonTypedScalarSerializerBase<Boolean> { private static final long serialVersionUID = 5817795880782727569L; public TrueOnlyBooleanSerializer() { super(Boolean.class); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(java.lang.Object) */ public boolean isEmpty(Boolean value) { return isEmpty(null, value); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ public boolean isEmpty(SerializerProvider provider, Boolean value) { return value == null || Boolean.FALSE.equals(value); } /* * (non-Javadoc) * @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(Boolean value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { jgen.writeBoolean(value.booleanValue()); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.std.StdScalarSerializer#getSchema(com.fasterxml.jackson.databind.SerializerProvider, java.lang.reflect.Type) */ @Override public JsonNode getSchema(SerializerProvider provider, Type typeHint) { return createSchemaNode("boolean", true); } /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.ser.std.StdScalarSerializer#acceptJsonFormatVisitor(com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper, com.fasterxml.jackson.databind.JavaType) */ @Override public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { if (visitor != null) { visitor.expectBooleanFormat(typeHint); } } } /** * Helper to easily map embedded resources and find out whether they were curied. * * @author Oliver Gierke */ private static class EmbeddedMapper { private RelProvider relProvider; private CurieProvider curieProvider; private boolean preferCollectionRels; /** * Creates a new {@link EmbeddedMapper} for the given {@link RelProvider}, {@link CurieProvider} and flag whether to * prefer collection relations. * * @param relProvider must not be {@literal null}. * @param curieProvider can be {@literal null}. * @param preferCollectionRels */ public EmbeddedMapper(RelProvider relProvider, CurieProvider curieProvider, boolean preferCollectionRels) { Assert.notNull(relProvider, "RelProvider must not be null!"); this.relProvider = relProvider; this.curieProvider = curieProvider; this.preferCollectionRels = preferCollectionRels; } /** * Maps the given source elements as embedded values. * * @param source must not be {@literal null}. * @return */ public Map<String, Object> map(Iterable<?> source) { Assert.notNull(source, "Elements must not be null!"); HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, curieProvider, preferCollectionRels); for (Object resource : source) { builder.add(resource); } return builder.asMap(); } /** * Returns whether the given source elements will be namespaced. * * @param source must not be {@literal null}. * @return */ public boolean hasCuriedEmbed(Iterable<?> source) { for (String rel : map(source).keySet()) { if (rel.contains(":")) { return true; } } return false; } } static class HalLink { private final Link link; private final String title; public HalLink(Link link, String title) { this.link = link; this.title = title; } @JsonUnwrapped public Link getLink() { return link; } @JsonInclude(Include.NON_NULL) public String getTitle() { return title; } } }