Java tutorial
/* * Copyright 2012-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. * 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.hateoas.ImageName; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.MediaType; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.hateoas.core.JsonRelAsArray; import org.springframework.hateoas.core.JsonResourceAsArray; import org.springframework.util.Assert; 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; public Jackson2HalModule() { super("c4-ext-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 final BeanProperty property; private final CurieProvider curieProvider; public HalLinkListSerializer(CurieProvider curieProvider) { this(null, curieProvider); } public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider) { super(List.class, false); this.property = property; this.curieProvider = curieProvider; } /* * (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; for (Link link : value) { 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(link); } if (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); } /* * (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); } /* * (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) */ @Override public boolean isEmpty(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 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 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(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) */ @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; } // render as object if Link is not of type ArrayProfiledLink if (list.size() == 1) { if (!renderAsArray(list.get(0))) { serializeContents(list.iterator(), jgen, provider); return; } } jgen.writeStartArray(); serializeContents(list.iterator(), jgen, provider); jgen.writeEndArray(); } private boolean renderAsArray(Object object) { boolean renderAsAnArray = false; Class<?> clazz = object.getClass(); if (clazz.isAnnotationPresent(JsonRelAsArray.class)) { if (object instanceof Link) { String linkRelation = ((Link) object).getRel(); String[] relations = clazz.getAnnotation(JsonRelAsArray.class).value(); for (String relation : relations) { if (relation.equals(linkRelation)) { renderAsAnArray = true; break; } } } } return renderAsAnArray; } 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) */ @Override public boolean isEmpty(Object arg0) { 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; @SuppressWarnings("deprecation") public HalLinkListDeserializer() { super(List.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); if (isImageLink(link)) { String name = link.getName() != null ? link.getName() : ImageName.DEFAULT.getName(); String type = link.getType() != null ? link.getType() : MediaType.IMAGE_JPEG.getContentType(); result.add(new Link(link.getHref(), relation, name, link.getTitle(), type)); } else { String profile = link.getProfile(); if (profile != null) { result.add(new Link(link.getHref(), relation, profile, link.getName())); } else { result.add(new Link(link.getHref(), relation, link.getName(), link.getTitle(), link.getType())); } } } } else { link = jp.readValueAs(Link.class); if (isImageLink(link)) { String name = link.getName() != null ? link.getName() : ImageName.DEFAULT.getName(); String type = link.getType() != null ? link.getType() : MediaType.IMAGE_JPEG.getContentType(); result.add(new Link(link.getHref(), relation, name, link.getTitle(), type)); } else { String profile = link.getProfile(); if (profile != null) { result.add(new Link(link.getHref(), relation, profile, link.getName())); } else { result.add(new Link(link.getHref(), relation, link.getName(), link.getTitle(), link.getType())); } } } } return result; } private boolean isImageLink(Link link) { if (link.getTitle() != null) { return true; } return false; } } /** * Custom {@link JsonSerializer} to render * {@link org.springframework.hateoas.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 final BeanProperty property; private final RelProvider relProvider; private final boolean enforceEmbeddedCollections; public HalResourcesSerializer(RelProvider relPorvider, boolean enforceEmbeddedCollections) { this(null, relPorvider, enforceEmbeddedCollections); } public HalResourcesSerializer(BeanProperty property, RelProvider relProvider, boolean enforceEmbeddedCollections) { super(Collection.class, false); this.property = property; this.relProvider = relProvider; this.enforceEmbeddedCollections = enforceEmbeddedCollections; } @Override public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { boolean overriddenEnforceEmbeddedCollections = enforceEmbeddedCollections; if (!value.isEmpty()) { Object content = value.iterator().next(); overriddenEnforceEmbeddedCollections = content.getClass() .isAnnotationPresent(JsonResourceAsArray.class); } HalEmbeddedBuilder builder = new HalEmbeddedBuilder(relProvider, overriddenEnforceEmbeddedCollections); for (Object resource : value) { builder.add(resource); } provider.findValueSerializer(Map.class, property).serialize(builder.asMap(), jgen, provider); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { return new HalResourcesSerializer(property, relProvider, enforceEmbeddedCollections); } @Override public JavaType getContentType() { return null; } @Override public JsonSerializer<?> getContentSerializer() { return null; } @Override public boolean isEmpty(Collection<?> value) { return value.isEmpty(); } @Override public boolean hasSingleElement(Collection<?> value) { return value.size() == 1; } @Override protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) { return null; } } public static class HalResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements ContextualDeserializer { private static final long serialVersionUID = 4755806754621032622L; private final JavaType contentType; public HalResourcesDeserializer() { this(List.class, null); } public HalResourcesDeserializer(JavaType vc) { this(null, vc); } @SuppressWarnings("deprecation") private HalResourcesDeserializer(Class<?> 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; } } public static class HalHandlerInstantiator extends HandlerInstantiator { 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!"); HalResourcesSerializer resourcesSerializer = new HalResourcesSerializer(resolver, enforceEmbeddedCollections); HalLinkListSerializer linkListSerializer = new HalLinkListSerializer(curieProvider); // customised this.instanceMap.put(HalResourcesSerializer.class, resourcesSerializer); this.instanceMap.put(HalLinkListSerializer.class, linkListSerializer); // overriden this.instanceMap.put(org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer.class, linkListSerializer); this.instanceMap.put(org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesSerializer.class, resourcesSerializer); } private Object findInstance(Class<?> type) { Object result = instanceMap.get(type); return result != null ? result : BeanUtils.instantiateClass(type); } /* * (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); } } /** * {@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> { public TrueOnlyBooleanSerializer() { super(Boolean.class); } @Override public boolean isEmpty(Boolean value) { return (value == null) || Boolean.FALSE.equals(value); } @Override public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { jgen.writeBoolean(value.booleanValue()); } @Override public JsonNode getSchema(SerializerProvider provider, Type typeHint) { return createSchemaNode("boolean", true); } @Override public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { if (visitor != null) { visitor.expectBooleanFormat(typeHint); } } } }