#1447 - Overhaul Jackson customizations for HAL FORMS.

Got rid of quite a few custom serializers in Jackson2HalFormsModule. We now rely on the HAL setup mostly and register a virtual bean property to render the _templates field. The Jackson component used for that is registered as Spring bean so that we can now use the HAL HandlerInstantiator with the Jackson2HalFormsModule.

Moved quite a few integration tests to the COntextualMapper API.
This commit is contained in:
Oliver Drotbohm
2021-01-22 21:58:43 +01:00
parent b7877f2514
commit 3da23a7d76
23 changed files with 468 additions and 1342 deletions

View File

@@ -10,7 +10,6 @@
"_templates" : {
"default" : {
"method" : "put",
"contentType" : "",
"properties" : [ {
"name" : "firstName",
"required" : true
@@ -24,7 +23,6 @@
},
"partiallyUpdateEmployee" : {
"method" : "patch",
"contentType" : "",
"properties" : [ {
"name" : "firstName",
"required" : false

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2021 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
*
* https://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.mediatype;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter;
/**
* A {@link HandlerInstantiator} that will use instances explicitly registered with it but fall back to lookup or even
* create a fresh instance via the {@link AutowireCapableBeanFactory} provided on construction.
*
* @author Oliver Drotbohm
*/
@SuppressWarnings("null")
public class ConfigurableHandlerInstantiator extends HandlerInstantiator {
private final Map<Class<?>, Object> instances = new HashMap<>();
private final AutowireCapableBeanFactory beanFactory;
/**
* Creates a new {@link ConfigurableHandlerInstantiator} for the given {@link AutowireCapableBeanFactory}.
*
* @param beanFactory must not be {@literal null}.
*/
protected ConfigurableHandlerInstantiator(AutowireCapableBeanFactory beanFactory) {
Assert.notNull(beanFactory, "BeanFactory must not be null!");
this.beanFactory = beanFactory;
}
protected void registerInstance(Object instance) {
this.instances.put(instance.getClass(), instance);
}
@Nullable
@SuppressWarnings("unchecked")
protected <T> T findInstance(Class<T> type) {
return (T) this.instances.get(type);
}
@SuppressWarnings("unchecked")
protected <T> T findOrCreateInstance(Class<T> type) {
Object object = findInstance(type);
return object != null
? (T) object
: beanFactory.getBeanProvider(type)
.getIfAvailable(() -> beanFactory.createBean(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<?>) findOrCreateInstance(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) findOrCreateInstance(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<?>) findOrCreateInstance(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<?>) findOrCreateInstance(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) findOrCreateInstance(resolverClass);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#virtualPropertyWriterInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, java.lang.Class)
*/
@Override
public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig<?> config, Class<?> implClass) {
return (VirtualBeanPropertyWriter) findOrCreateInstance(implClass);
}
}

View File

@@ -34,7 +34,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @author Greg Turnquist
*/
@JsonPropertyOrder({ "content", "links" })
abstract class CollectionModelMixin<T> extends CollectionModel<T> {
public abstract class CollectionModelMixin<T> extends CollectionModel<T> {
@Override
@JsonProperty("_embedded")

View File

@@ -26,23 +26,24 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.ConfigurableHandlerInstantiator;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
@@ -51,14 +52,9 @@ import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase;
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;
@@ -711,32 +707,32 @@ public class Jackson2HalModule extends SimpleModule {
*
* @author Oliver Gierke
*/
public static class HalHandlerInstantiator extends HandlerInstantiator {
private final Map<Class<?>, Object> serializers = new HashMap<>();
private final @Nullable AutowireCapableBeanFactory delegate;
public static class HalHandlerInstantiator extends ConfigurableHandlerInstantiator {
/**
* Convenience constructor for testing purposes. Prefer
* {@link #HalHandlerInstantiator(LinkRelationProvider, CurieProvider, MessageResolver, HalConfiguration, AutowireCapableBeanFactory)}
*
* @param provider must not be {@literal null}.
* @param curieProvider must not be {@literal null}.
* @param resolver must not be {@literal null}.
*/
public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider,
MessageResolver resolver) {
this(provider, curieProvider, resolver, new HalConfiguration());
this(provider, curieProvider, resolver, new HalConfiguration(), new DefaultListableBeanFactory());
}
/**
* Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider}
* and {@link MessageResolver}. 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 curieProvider must not be {@literal null}.
* @param resolver must not be {@literal null}.
* @param halConfiguration must not be {@literal null}.
* @param delegate must not be {@literal null}.
*/
public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver,
HalConfiguration halConfiguration) {
this(provider, curieProvider, resolver, halConfiguration, null);
}
HalConfiguration halConfiguration, AutowireCapableBeanFactory delegate) {
public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver,
HalConfiguration halConfiguration, @Nullable AutowireCapableBeanFactory delegate) {
super(delegate);
Assert.notNull(provider, "RelProvider must not be null!");
Assert.notNull(curieProvider, "CurieProvider must not be null!");
@@ -744,73 +740,8 @@ public class Jackson2HalModule extends SimpleModule {
EmbeddedMapper mapper = new EmbeddedMapper(provider, curieProvider,
halConfiguration.isEnforceEmbeddedCollections());
this.delegate = delegate;
this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper, halConfiguration));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration));
}
/*
* (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
@SuppressWarnings("null")
public JsonDeserializer<?> deserializerInstance(@NonNull DeserializationConfig config, @NonNull Annotated annotated,
@NonNull 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
@SuppressWarnings("null")
public KeyDeserializer keyDeserializerInstance(@NonNull DeserializationConfig config, @NonNull Annotated annotated,
@NonNull 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
@SuppressWarnings("null")
public JsonSerializer<?> serializerInstance(@NonNull SerializationConfig config, @NonNull Annotated annotated,
@NonNull 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
@SuppressWarnings("null")
public TypeResolverBuilder<?> typeResolverBuilderInstance(@NonNull MapperConfig<?> config,
@NonNull Annotated annotated, @NonNull 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
@SuppressWarnings("null")
public TypeIdResolver typeIdResolverInstance(@NonNull MapperConfig<?> config, @NonNull Annotated annotated,
@NonNull 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);
registerInstance(new HalResourcesSerializer(mapper, halConfiguration));
registerInstance(new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration));
}
}
@@ -988,6 +919,7 @@ public class Jackson2HalModule extends SimpleModule {
@Nullable
@JsonInclude(Include.NON_EMPTY)
@JsonProperty
public String getTitle() {
return title;
}

View File

@@ -1,379 +0,0 @@
/*
* Copyright 2017-2021 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
*
* https://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.mediatype.hal.forms;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.PagedModel.PageMetadata;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.PropertyUtils;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsModule.HalFormsLinksDeserializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Representation of a HAL-FORMS document.
*
* @author Dietrich Schulten
* @author Greg Turnquist
* @author Oliver Gierke
*/
@JsonPropertyOrder({ "attributes", "entity", "entities", "embedded", "links", "templates", "metadata" })
final class HalFormsDocument<T> {
@Nullable //
@JsonInclude(Include.NON_EMPTY) //
private final Map<String, Object> attributes;
@Nullable //
@JsonUnwrapped //
@JsonInclude(Include.NON_NULL) //
private final T entity;
@Nullable //
@JsonInclude(Include.NON_EMPTY) //
@JsonIgnore //
private final Collection<T> entities;
@JsonProperty("_embedded") //
@JsonInclude(Include.NON_EMPTY) //
private final Map<HalLinkRelation, Object> embedded;
@Nullable //
@JsonProperty("page") //
@JsonInclude(Include.NON_NULL) //
private final PagedModel.PageMetadata pageMetadata;
@JsonProperty("_links") //
@JsonInclude(Include.NON_EMPTY) //
@JsonSerialize(using = HalLinkListSerializer.class) //
@JsonDeserialize(using = HalFormsLinksDeserializer.class) //
private final Links links;
@JsonProperty("_templates") //
@JsonInclude(Include.NON_EMPTY) //
private final Map<String, HalFormsTemplate> templates;
HalFormsDocument(Map<String, Object> attributes, T entity, Collection<T> entities,
Map<HalLinkRelation, Object> embedded, PageMetadata pageMetadata, Links links,
Map<String, HalFormsTemplate> templates) {
this.attributes = attributes;
this.entity = entity;
this.entities = entities;
this.embedded = embedded;
this.pageMetadata = pageMetadata;
this.links = links;
this.templates = templates;
}
private HalFormsDocument() {
this(null, null, null, Collections.emptyMap(), null, Links.NONE, Collections.emptyMap());
}
/**
* Creates a new {@link HalFormsDocument} for the given resource support.
*
* @param model can be {@literal null}
* @return
*/
static HalFormsDocument<?> forRepresentationModel(RepresentationModel<?> model) {
Map<String, Object> attributes = PropertyUtils.extractPropertyValues(model);
attributes.remove("links");
return new HalFormsDocument<>().withAttributes(attributes);
}
/**
* Creates a new {@link HalFormsDocument} for the given resource.
*
* @param resource can be {@literal null}.
* @return
*/
static <T> HalFormsDocument<T> forEntity(@Nullable T resource) {
return new HalFormsDocument<T>().withEntity(resource);
}
/**
* returns a new {@link HalFormsDocument} for the given resources.
*
* @param entities must not be {@literal null}.
* @return
*/
static <T> HalFormsDocument<T> forEntities(Collection<T> entities) {
Assert.notNull(entities, "Resources must not be null!");
return new HalFormsDocument<T>().withEntities(entities);
}
/**
* Creates a new empty {@link HalFormsDocument}.
*
* @return
*/
static HalFormsDocument<?> empty() {
return new HalFormsDocument<>();
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal attributes}.
*
* @param attributes
* @return
*/
private HalFormsDocument<T> withAttributes(@Nullable Map<String, Object> attributes) {
return this.attributes == attributes ? this
: new HalFormsDocument<T>(attributes, this.entity, this.entities, this.embedded, this.pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing {@literal entity}.
*
* @param entity
* @return
*/
private HalFormsDocument<T> withEntity(@Nullable T entity) {
return this.entity == entity ? this
: new HalFormsDocument<T>(this.attributes, entity, this.entities, this.embedded, this.pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal entities}.
*
* @param entities
* @return
*/
private HalFormsDocument<T> withEntities(@Nullable Collection<T> entities) {
return this.entities == entities ? this
: new HalFormsDocument<T>(this.attributes, this.entity, entities, this.embedded, this.pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying the attributes and adding a new embedded value.
*
* @param key must not be {@literal null} or empty.
* @param value must not be {@literal null}.
* @return
*/
HalFormsDocument<T> andEmbedded(HalLinkRelation key, Object value) {
Assert.notNull(key, "Embedded key must not be null!");
Assert.notNull(value, "Embedded value must not be null!");
Map<HalLinkRelation, Object> embedded = new HashMap<>(this.embedded);
embedded.put(key, value);
return new HalFormsDocument<>(this.attributes, this.entity, this.entities, embedded, this.pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing all {@literal embedded}s.
*
* @param embedded
* @return
*/
HalFormsDocument<T> withEmbedded(Map<HalLinkRelation, Object> embedded) {
return this.embedded == embedded ? this
: new HalFormsDocument<T>(this.attributes, this.entity, this.entities, embedded, this.pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal pageMetadata}.
*
* @param pageMetadata
* @return
*/
HalFormsDocument<T> withPageMetadata(@Nullable PageMetadata pageMetadata) {
return this.pageMetadata == pageMetadata ? this
: new HalFormsDocument<T>(this.attributes, this.entity, this.entities, this.embedded, pageMetadata, this.links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying the attributes and adding a new {@link Link}.
*
* @param link must not be {@literal null}.
* @return
*/
HalFormsDocument<T> andLink(Link link) {
Assert.notNull(link, "Link must not be null!");
return new HalFormsDocument<>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata,
this.links.and(link), this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal links}.
*
* @param links
* @return
*/
HalFormsDocument<T> withLinks(Links links) {
return this.links == links ? this
: new HalFormsDocument<T>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, links,
this.templates);
}
/**
* Create a new {@link HalFormsDocument} by copying the attributes and adding a new {@link HalFormsTemplate}.
*
* @param name must not be {@literal null} or empty.
* @param template must not be {@literal null}.
* @return
*/
HalFormsDocument<T> andTemplate(String name, HalFormsTemplate template) {
Assert.hasText(name, "Template name must not be null or empty!");
Assert.notNull(template, "Template must not be null!");
Map<String, HalFormsTemplate> templates = new HashMap<>(this.templates);
templates.put(name, template);
return new HalFormsDocument<>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata,
this.links, templates);
}
/**
* Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal templates}.
*
* @param templates
* @return
*/
HalFormsDocument<T> withTemplates(Map<String, HalFormsTemplate> templates) {
return this.templates == templates ? this
: new HalFormsDocument<T>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata,
this.links, templates);
}
@Nullable
@JsonAnyGetter
Map<String, Object> getAttributes() {
return this.attributes;
}
@Nullable
T getEntity() {
return this.entity;
}
@Nullable
Collection<T> getEntities() {
return this.entities;
}
Map<HalLinkRelation, Object> getEmbedded() {
return this.embedded;
}
@Nullable
PageMetadata getPageMetadata() {
return this.pageMetadata;
}
Links getLinks() {
return this.links;
}
Map<String, HalFormsTemplate> getTemplates() {
return this.templates;
}
/**
* Returns the template with the given name.
*
* @param key must not be {@literal null}.
* @return
*/
@JsonIgnore
HalFormsTemplate getTemplate(String key) {
Assert.notNull(key, "Template key must not be null!");
return this.templates.get(key);
}
/**
* Returns the default template of the document.
*
* @return
*/
@JsonIgnore
HalFormsTemplate getDefaultTemplate() {
return getTemplate(HalFormsTemplate.DEFAULT_KEY);
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof HalFormsDocument))
return false;
HalFormsDocument<?> that = (HalFormsDocument<?>) o;
return Objects.equals(this.attributes, that.attributes) && Objects.equals(this.entity, that.entity)
&& Objects.equals(this.entities, that.entities) && Objects.equals(this.embedded, that.embedded)
&& Objects.equals(this.pageMetadata, that.pageMetadata) && Objects.equals(this.links, that.links)
&& Objects.equals(this.templates, that.templates);
}
@Override
public int hashCode() {
return Objects.hash(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, this.links,
this.templates);
}
public String toString() {
return "HalFormsDocument(attributes=" + this.attributes + ", entity=" + this.entity + ", entities=" + this.entities
+ ", embedded=" + this.embedded + ", pageMetadata=" + this.pageMetadata + ", links=" + this.links
+ ", templates=" + this.templates + ")";
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.hateoas.mediatype.hal.forms;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
@@ -27,6 +28,7 @@ import org.springframework.hateoas.config.HypermediaMappingInformation;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider;
import org.springframework.http.MediaType;
@@ -39,7 +41,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
@Configuration
@Configuration(proxyBeanMethods = false)
class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
private final DelegatingLinkRelationProvider relProvider;
@@ -49,6 +51,8 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
private final MessageResolver resolver;
private final AbstractAutowireCapableBeanFactory beanFactory;
private HalFormsConfiguration resolvedConfiguration;
public HalFormsMediaTypeConfiguration(DelegatingLinkRelationProvider relProvider,
ObjectProvider<CurieProvider> curieProvider, ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
ObjectProvider<HalConfiguration> halConfiguration, MessageResolver resolver,
@@ -67,6 +71,15 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
return new HalFormsLinkDiscoverer();
}
@Bean
HalFormsTemplatePropertyWriter halFormsTemplatePropertyWriter() {
HalFormsConfiguration configuration = getResolvedConfiguration();
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, resolver);
return new HalFormsTemplatePropertyWriter(builder);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.config.HypermediaMappingInformation#configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper)
@@ -74,15 +87,15 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
@Override
public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
HalFormsConfiguration configuration = halFormsConfiguration
.getIfAvailable(() -> new HalFormsConfiguration(halConfiguration.getIfAvailable(HalConfiguration::new)));
HalFormsConfiguration halFormsConfig = getResolvedConfiguration();
CurieProvider provider = curieProvider.getIfAvailable(() -> CurieProvider.NONE);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider,
curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, configuration, beanFactory));
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, provider,
resolver, halFormsConfig.getHalConfiguration(), beanFactory));
configuration.customize(mapper);
halFormsConfig.customize(mapper);
return mapper;
}
@@ -95,4 +108,16 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
public List<MediaType> getMediaTypes() {
return HypermediaType.HAL_FORMS.getMediaTypes();
}
HalFormsConfiguration getResolvedConfiguration() {
Supplier<HalFormsConfiguration> defaultConfig = () -> new HalFormsConfiguration(
halConfiguration.getIfAvailable(HalConfiguration::new));
if (resolvedConfiguration == null) {
this.resolvedConfiguration = halFormsConfiguration.getIfAvailable(defaultConfig);
}
return resolvedConfiguration;
}
}

View File

@@ -43,7 +43,7 @@ import org.springframework.util.StringUtils;
* @since 1.3
* @soundtrack The Chicks - March March (Gaslighter)
*/
public class HalFormsPropertyFactory {
class HalFormsPropertyFactory {
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH);

View File

@@ -1,326 +0,0 @@
/*
* Copyright 2017-2021 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
*
* https://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.mediatype.hal.forms;
import java.io.IOException;
import java.util.Map;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.HalLinkRelation;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
/**
* Collection of components needed to serialize a HAL-FORMS document.
*
* @author Greg Turnquist
*/
class HalFormsSerializers {
static class HalFormsRepresentationModelSerializer extends ContainerSerializer<RepresentationModel<?>>
implements ContextualSerializer {
private static final long serialVersionUID = -4583146321934407153L;
private final HalFormsTemplateBuilder builder;
private final BeanProperty property;
HalFormsRepresentationModelSerializer(HalFormsTemplateBuilder builder, @Nullable BeanProperty property) {
super(RepresentationModel.class, false);
this.builder = builder;
this.property = property;
}
HalFormsRepresentationModelSerializer(HalFormsTemplateBuilder customizations) {
this(customizations, null);
}
/*
* (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
@SuppressWarnings("null")
public void serialize(RepresentationModel<?> value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
HalFormsDocument<?> doc = HalFormsDocument.forRepresentationModel(value) //
.withLinks(value.getLinks()) //
.withTemplates(builder.findTemplates(value));
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@Override
@Nullable
@SuppressWarnings("null")
public JavaType getContentType() {
return null;
}
@Override
@Nullable
@SuppressWarnings("null")
public JsonSerializer<?> getContentSerializer() {
return null;
}
@Override
@SuppressWarnings("null")
public boolean hasSingleElement(RepresentationModel<?> resource) {
return false;
}
@Override
@Nullable
@SuppressWarnings("null")
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer typeSerializer) {
return null;
}
@Override
@SuppressWarnings("null")
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
return new HalFormsRepresentationModelSerializer(builder, property);
}
}
/**
* Serializer for {@link CollectionModel}.
*/
static class HalFormsEntityModelSerializer extends ContainerSerializer<EntityModel<?>>
implements ContextualSerializer {
private static final long serialVersionUID = -7912243216469101379L;
private final HalFormsTemplateBuilder builder;
private final BeanProperty property;
HalFormsEntityModelSerializer(HalFormsTemplateBuilder builder, @Nullable BeanProperty property) {
super(EntityModel.class, false);
this.builder = builder;
this.property = property;
}
HalFormsEntityModelSerializer(HalFormsTemplateBuilder builder) {
this(builder, null);
}
/*
* (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
@SuppressWarnings("null")
public void serialize(EntityModel<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
HalFormsDocument<?> doc = HalFormsDocument.forEntity(value.getContent()) //
.withLinks(value.getLinks()) //
.withTemplates(builder.findTemplates(value));
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
@Nullable
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
@Nullable
public JsonSerializer<?> getContentSerializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
@SuppressWarnings("null")
public boolean hasSingleElement(EntityModel<?> resource) {
return false;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
@Nullable
@SuppressWarnings("null")
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer typeSerializer) {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
@SuppressWarnings("null")
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalFormsEntityModelSerializer(builder, property);
}
}
/**
* Serializer for {@link CollectionModel}
*/
static class HalFormsCollectionModelSerializer extends ContainerSerializer<CollectionModel<?>>
implements ContextualSerializer {
private static final long serialVersionUID = -3601146866067500734L;
private final BeanProperty property;
private final Jackson2HalModule.EmbeddedMapper embeddedMapper;
private final HalFormsTemplateBuilder customizations;
private final HalConfiguration configuration;
HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations,
Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration,
@Nullable BeanProperty property) {
super(CollectionModel.class, false);
this.property = property;
this.embeddedMapper = embeddedMapper;
this.customizations = customizations;
this.configuration = configuration;
}
HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations,
Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration) {
this(customizations, embeddedMapper, configuration, null);
}
/*
* (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
@SuppressWarnings("null")
public void serialize(CollectionModel<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() //
? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) //
: embeddedMapper;
Map<HalLinkRelation, Object> embeddeds = mapper.map(value);
HalFormsDocument<?> doc;
if (value instanceof PagedModel) {
doc = HalFormsDocument.empty() //
.withEmbedded(embeddeds) //
.withPageMetadata(((PagedModel<?>) value).getMetadata()) //
.withLinks(value.getLinks()) //
.withTemplates(customizations.findTemplates(value));
} else {
doc = HalFormsDocument.empty() //
.withEmbedded(embeddeds) //
.withLinks(value.getLinks()) //
.withTemplates(customizations.findTemplates(value));
}
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
@Nullable
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
@Nullable
public JsonSerializer<?> getContentSerializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
@SuppressWarnings("null")
public boolean hasSingleElement(CollectionModel<?> resources) {
return resources.getContent().size() == 1;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
@Nullable
@SuppressWarnings("null")
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer typeSerializer) {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
@SuppressWarnings("null")
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalFormsCollectionModelSerializer(customizations, embeddedMapper, configuration, property);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2021 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
*
* https://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.mediatype.hal.forms;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter;
import com.fasterxml.jackson.databind.util.Annotations;
/**
* @author Oliver Drotbohm
*/
@SuppressWarnings("null")
class HalFormsTemplatePropertyWriter extends VirtualBeanPropertyWriter {
private static final long serialVersionUID = 6271264033606657428L;
private final HalFormsTemplateBuilder builder;
/**
* @param builder must not be {@literal null}.
*/
public HalFormsTemplatePropertyWriter(HalFormsTemplateBuilder builder) {
Assert.notNull(builder, "HalFormsTemplateBuilder must not be null!");
this.builder = builder;
}
/**
* @param builder2
* @param config
* @param declaringClass
* @param propDef
* @param type
*/
public HalFormsTemplatePropertyWriter(HalFormsTemplateBuilder builder, MapperConfig<?> config,
Annotations annotations, BeanPropertyDefinition propDef, JavaType type) {
super(propDef, annotations, type);
this.builder = builder;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter#value(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
protected Object value(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
RepresentationModel<?> model = (RepresentationModel<?>) bean;
return builder.findTemplates(model);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter#withConfig(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.AnnotatedClass, com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition, com.fasterxml.jackson.databind.JavaType)
*/
@Override
public VirtualBeanPropertyWriter withConfig(MapperConfig<?> config, AnnotatedClass declaringClass,
BeanPropertyDefinition propDef, JavaType type) {
return new HalFormsTemplatePropertyWriter(builder, config, declaringClass.getAnnotations(), propDef, type);
}
}

View File

@@ -15,57 +15,22 @@
*/
package org.springframework.hateoas.mediatype.hal.forms;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListDeserializer;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.mediatype.hal.CollectionModelMixin;
import org.springframework.hateoas.mediatype.hal.LinkMixin;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsDeserializers.HalFormsCollectionModelDeserializer;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsCollectionModelSerializer;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsEntityModelSerializer;
import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsRepresentationModelSerializer;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.hateoas.server.mvc.JacksonSerializers.MediaTypeDeserializer;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.annotation.JsonAppend;
import com.fasterxml.jackson.databind.annotation.JsonAppend.Prop;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
/**
* Serialize / deserialize all the parts of HAL-FORMS documents using Jackson.
@@ -83,176 +48,16 @@ public class Jackson2HalFormsModule extends SimpleModule {
super("hal-forms-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(Links.class, LinksMixin.class);
setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class);
setMixInAnnotation(EntityModel.class, EntityModelMixin.class);
setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class);
setMixInAnnotation(PagedModel.class, PagedModelMixin.class);
setMixInAnnotation(MediaType.class, MediaTypeMixin.class);
}
@JsonSerialize(using = HalLinkListSerializer.class)
abstract class LinksMixin {}
@JsonSerialize(using = HalFormsRepresentationModelSerializer.class)
@JsonAppend(
props = @Prop(name = "_templates", value = HalFormsTemplatePropertyWriter.class, include = Include.NON_EMPTY))
abstract class RepresentationModelMixin extends org.springframework.hateoas.mediatype.hal.RepresentationModelMixin {}
@JsonSerialize(using = HalFormsEntityModelSerializer.class)
abstract class EntityModelMixin<T> extends EntityModel<T> {}
@JsonSerialize(using = HalFormsCollectionModelSerializer.class)
abstract class CollectionModelMixin<T> extends CollectionModel<T> {
@Override
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonDeserialize(using = HalFormsCollectionModelDeserializer.class)
public abstract Collection<T> getContent();
}
abstract class PagedModelMixin<T> extends PagedModel<T> {
@Nullable
@Override
@JsonProperty("page")
@JsonInclude(Include.NON_EMPTY)
public PageMetadata getMetadata() {
return super.getMetadata();
}
}
@JsonSerialize(using = ToStringSerializer.class)
@JsonDeserialize(using = MediaTypeDeserializer.class)
interface MediaTypeMixin {}
static class HalFormsLinksDeserializer extends ContainerDeserializerBase<Links> {
private static final long serialVersionUID = -848240531474910385L;
private final HalLinkListDeserializer delegate = new HalLinkListDeserializer();
public HalFormsLinksDeserializer() {
super(TypeFactory.defaultInstance().constructType(Links.class));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
@Nullable
public JsonDeserializer<Object> getContentDeserializer() {
return delegate.getContentDeserializer();
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
@SuppressWarnings("null")
public Links deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Links.of(delegate.deserialize(p, ctxt));
}
}
/**
* Create new HAL-FORMS serializers based on the context.
*/
public static class HalFormsHandlerInstantiator extends HalHandlerInstantiator {
private final Map<Class<?>, Object> serializers = new HashMap<>();
public HalFormsHandlerInstantiator(LinkRelationProvider resolver, CurieProvider curieProvider,
MessageResolver accessor, HalFormsConfiguration configuration, AutowireCapableBeanFactory beanFactory) {
super(resolver, curieProvider, accessor, configuration.getHalConfiguration(), beanFactory);
HalConfiguration halConfiguration = configuration.getHalConfiguration();
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider,
halConfiguration.isEnforceEmbeddedCollections());
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, accessor);
this.serializers.put(HalFormsRepresentationModelSerializer.class,
new HalFormsRepresentationModelSerializer(builder));
this.serializers.put(HalFormsEntityModelSerializer.class, new HalFormsEntityModelSerializer(builder));
this.serializers.put(HalFormsCollectionModelSerializer.class,
new HalFormsCollectionModelSerializer(builder, mapper, halConfiguration));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration));
}
public HalFormsHandlerInstantiator(LinkRelationProvider relProvider, CurieProvider curieProvider,
MessageResolver resolver, AutowireCapableBeanFactory beanFactory) {
this(relProvider, curieProvider, resolver, beanFactory.getBean(HalFormsConfiguration.class), beanFactory);
}
@Nullable
private Object findInstance(Class<?> type) {
return this.serializers.get(type);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#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) {
Object jsonDeser = findInstance(deserClass);
return jsonDeser != null ? (JsonDeserializer<?>) jsonDeser
: super.deserializerInstance(config, annotated, deserClass);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#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) {
Object keyDeser = findInstance(keyDeserClass);
return keyDeser != null ? (KeyDeserializer) keyDeser
: super.keyDeserializerInstance(config, annotated, keyDeserClass);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#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) {
Object jsonSer = findInstance(serClass);
return jsonSer != null ? (JsonSerializer<?>) jsonSer : super.serializerInstance(config, annotated, serClass);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#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) {
Object builder = findInstance(builderClass);
return builder != null ? (TypeResolverBuilder<?>) builder
: super.typeResolverBuilderInstance(config, annotated, builderClass);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#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) {
Object resolver = findInstance(resolverClass);
return resolver != null ? (TypeIdResolver) resolver
: super.typeIdResolverInstance(config, annotated, resolverClass);
}
}
}

View File

@@ -19,11 +19,6 @@ import java.io.StringWriter;
import java.io.Writer;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -44,16 +39,6 @@ public abstract class AbstractJackson2MarshallingIntegrationTest {
mapper = MappingTestUtils.defaultObjectMapper();
}
protected ObjectMapper with(HalConfiguration configuration) {
ObjectMapper copy = mapper.copy();
copy.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
MessageResolver.DEFAULTS_ONLY, configuration));
return copy;
}
protected String write(Object object) throws Exception {
Writer writer = new StringWriter();
mapper.writeValue(writer, object);

View File

@@ -86,27 +86,45 @@ public class MappingTestUtils {
}
}
public RepresentationModel<?> readObject(String filename) {
return readObject(filename, RepresentationModel.class);
public RepresentationModel<?> readObject(String serialized) {
try {
return mapper.readValue(serialized, RepresentationModel.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public <T> T readObject(String filename, Class<T> type) {
public RepresentationModel<?> readFile(String filename) {
return readFile(filename, RepresentationModel.class);
}
public <T> T readFile(String filename, Class<T> type) {
TypeFactory factory = mapper.getTypeFactory();
JavaType javaType = factory.constructType(type);
return readObject(filename, javaType);
return readFile(filename, javaType);
}
public <S> S readObject(String filename, Class<?> type, Class<?> elementType) {
public <S> S readFile(String filename, Class<?> type, Class<?> elementType) {
TypeFactory factory = mapper.getTypeFactory();
JavaType javaType = factory.constructParametricType(type, elementType);
return readObject(filename, javaType);
return readFile(filename, javaType);
}
public <S> S readObject(String filename, JavaType type) {
public <S> S readFile(String filename, Class<?> type, Class<?> elementType, Class<?> nested) {
TypeFactory factory = mapper.getTypeFactory();
JavaType genericElement = factory.constructParametricType(elementType, nested);
JavaType javaType = factory.constructParametricType(type, genericElement);
return readFile(filename, javaType);
}
public <S> S readFile(String filename, JavaType type) {
ClassPathResource resource = new ClassPathResource(filename, context);
@@ -119,7 +137,7 @@ public class MappingTestUtils {
}
}
public String readFile(String filename) {
public String readFileContent(String filename) {
ClassPathResource resource = new ClassPathResource(filename, context);

View File

@@ -37,7 +37,6 @@ import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.hal.SimplePojo;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
@@ -71,7 +70,7 @@ class Jackson2CollectionJsonIntegrationTest {
RepresentationModel<?> resourceSupport = new RepresentationModel<>();
resourceSupport.add(Link.of("localhost").withSelfRel());
assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFile("resource-support.json"));
assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFileContent("resource-support.json"));
}
@Test
@@ -80,7 +79,7 @@ class Jackson2CollectionJsonIntegrationTest {
RepresentationModel<?> expected = new RepresentationModel<>();
expected.add(Link.of("localhost"));
assertThat(mapper.readObject("resource-support.json")).isEqualTo(expected);
assertThat(mapper.readFile("resource-support.json")).isEqualTo(expected);
}
@Test
@@ -90,7 +89,7 @@ class Jackson2CollectionJsonIntegrationTest {
resourceSupport.add(Link.of("localhost"));
resourceSupport.add(Link.of("localhost2").withRel("orders"));
assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFile("resource-support-2.json"));
assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFileContent("resource-support-2.json"));
}
@Test
@@ -99,7 +98,7 @@ class Jackson2CollectionJsonIntegrationTest {
ResourceWithAttributes resource = new ResourceWithAttributes("test value");
resource.add(Link.of("localhost").withSelfRel());
assertThat(mapper.writeObject(resource)).isEqualTo(mapper.readFile("resource-support-3.json"));
assertThat(mapper.writeObject(resource)).isEqualTo(mapper.readFileContent("resource-support-3.json"));
}
@Test
@@ -108,7 +107,7 @@ class Jackson2CollectionJsonIntegrationTest {
ResourceWithAttributes expected = new ResourceWithAttributes("test value");
expected.add(Link.of("localhost").withSelfRel());
assertThat(mapper.readObject("resource-support-3.json", ResourceWithAttributes.class)).isEqualTo(expected);
assertThat(mapper.readFile("resource-support-3.json", ResourceWithAttributes.class)).isEqualTo(expected);
}
@Test
@@ -118,7 +117,7 @@ class Jackson2CollectionJsonIntegrationTest {
expected.add(Link.of("localhost"));
expected.add(Link.of("localhost2").withRel("orders"));
assertThat(mapper.readObject("resource-support-2.json").getLinks()).containsAll(expected.getLinks());
assertThat(mapper.readFile("resource-support-2.json").getLinks()).containsAll(expected.getLinks());
}
@Test
@@ -131,7 +130,7 @@ class Jackson2CollectionJsonIntegrationTest {
CollectionModel<String> resources = CollectionModel.of(content);
resources.add(Link.of("localhost"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources.json"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources.json"));
}
@Test
@@ -144,7 +143,7 @@ class Jackson2CollectionJsonIntegrationTest {
CollectionModel<String> expected = CollectionModel.of(content);
expected.add(Link.of("localhost"));
CollectionModel<String> result = mapper.readObject("resources.json", CollectionModel.class, String.class);
CollectionModel<String> result = mapper.readFile("resources.json", CollectionModel.class, String.class);
assertThat(result).isEqualTo(expected);
}
@@ -153,13 +152,13 @@ class Jackson2CollectionJsonIntegrationTest {
void renderResource() {
assertThat(mapper.writeObject(EntityModel.of("first", Link.of("localhost")))) //
.isEqualTo(mapper.readFile("resource.json"));
.isEqualTo(mapper.readFileContent("resource.json"));
}
@Test
void deserializeResource() {
EntityModel<String> actual = mapper.readObject("resource.json", EntityModel.class, String.class);
EntityModel<String> actual = mapper.readFile("resource.json", EntityModel.class, String.class);
assertThat(actual).isEqualTo(EntityModel.of("first", Link.of("localhost")));
}
@@ -175,7 +174,7 @@ class Jackson2CollectionJsonIntegrationTest {
resources.add(Link.of("localhost"));
resources.add(Link.of("/page/2").withRel("next"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources-with-resource-objects.json"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources-with-resource-objects.json"));
}
@Test
@@ -189,10 +188,8 @@ class Jackson2CollectionJsonIntegrationTest {
expected.add(Link.of("localhost"));
expected.add(Link.of("/page/2").withRel("next"));
JavaType entityModel = mapper.getGenericType(EntityModel.class, String.class);
JavaType collectionModel = mapper.getGenericType(CollectionModel.class, entityModel);
CollectionModel<?> actual = mapper.readObject("resources-with-resource-objects.json", collectionModel);
CollectionModel<?> actual = mapper.readFile("resources-with-resource-objects.json", CollectionModel.class,
EntityModel.class, String.class);
assertThat(actual).isEqualTo(expected);
}
@@ -208,25 +205,21 @@ class Jackson2CollectionJsonIntegrationTest {
resources.add(Link.of("localhost"));
resources.add(Link.of("/page/2").withRel("next"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources-simple-pojos.json"));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources-simple-pojos.json"));
}
@Test
void serializesPagedResource() throws Exception {
assertThat(mapper.writeObject(setupAnnotatedPagedResources())) //
.isEqualTo(mapper.readFile("paged-resources.json"));
.isEqualTo(mapper.readFileContent("paged-resources.json"));
}
@Test
void deserializesPagedResource() throws Exception {
JavaType entityModel = mapper.getGenericType(EntityModel.class, SimplePojo.class);
JavaType pagedModel = mapper.getGenericType(PagedModel.class, entityModel);
mapper.readObject("paged-resources.json", pagedModel);
PagedModel<?> result = mapper.readObject("paged-resources.json", pagedModel);
PagedModel<?> result = mapper.readFile("paged-resources.json", PagedModel.class, EntityModel.class,
SimplePojo.class);
assertThat(result).isEqualTo(setupAnnotatedPagedResources());
}

View File

@@ -99,7 +99,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model))
.isEqualTo(contextualMapper.readFile("hal-embedded-author-illustrator.json"));
.isEqualTo(contextualMapper.readFileContent("hal-embedded-author-illustrator.json"));
}
@Test // #864
@@ -119,7 +119,7 @@ public class HalModelBuilderUnitTest {
.forLink(Link.of("/people/john-smith", ILLUSTRATOR_REL)).build();
assertThat(this.mapper.writeValueAsString(model))
.isEqualTo(contextualMapper.readFile("hal-embedded-author-illustrator.json"));
.isEqualTo(contextualMapper.readFileContent("hal-embedded-author-illustrator.json"));
}
@Test // #864
@@ -130,7 +130,7 @@ public class HalModelBuilderUnitTest {
.link(ALAN_WATTS_SELF) //
.build();
assertThat(this.mapper.writeValueAsString(model)).isEqualTo(contextualMapper.readFile("hal-single-item.json"));
assertThat(this.mapper.writeValueAsString(model)).isEqualTo(contextualMapper.readFileContent("hal-single-item.json"));
}
@Test // #864
@@ -142,7 +142,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model)) //
.isEqualTo(contextualMapper.readFile("hal-single-item.json"));
.isEqualTo(contextualMapper.readFileContent("hal-single-item.json"));
}
@Test // #864
@@ -173,7 +173,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model))
.isEqualTo(contextualMapper.readFile("hal-embedded-collection.json"));
.isEqualTo(contextualMapper.readFileContent("hal-embedded-collection.json"));
}
@Test // #864
@@ -199,7 +199,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model))
.isEqualTo(contextualMapper.readFile("hal-embedded-collection.json"));
.isEqualTo(contextualMapper.readFileContent("hal-embedded-collection.json"));
}
@Test
@@ -208,7 +208,7 @@ public class HalModelBuilderUnitTest {
HalModelBuilder halModelBuilder = halModel();
assertThat(this.mapper.writeValueAsString(halModelBuilder.build()))
.isEqualTo(contextualMapper.readFile("hal-empty.json"));
.isEqualTo(contextualMapper.readFileContent("hal-empty.json"));
halModelBuilder //
.entity(halModel() //
@@ -218,13 +218,13 @@ public class HalModelBuilderUnitTest {
.build());
assertThat(this.mapper.writeValueAsString(halModelBuilder.build()))
.isEqualTo(contextualMapper.readFile("hal-one-thing.json"));
.isEqualTo(contextualMapper.readFileContent("hal-one-thing.json"));
halModelBuilder //
.embed(new Product("Alf alarm clock", 19.99), LinkRelation.of("product")).build();
assertThat(this.mapper.writeValueAsString(halModelBuilder.build()))
.isEqualTo(contextualMapper.readFile("hal-two-things.json"));
.isEqualTo(contextualMapper.readFileContent("hal-two-things.json"));
}
@Test // #193
@@ -239,7 +239,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model)) //
.isEqualTo(contextualMapper.readFile("hal-multiple-types.json"));
.isEqualTo(contextualMapper.readFileContent("hal-multiple-types.json"));
}
@Test // #193
@@ -260,7 +260,7 @@ public class HalModelBuilderUnitTest {
.build();
assertThat(this.mapper.writeValueAsString(model))
.isEqualTo(contextualMapper.readFile("hal-explicit-and-implicit-relations.json"));
.isEqualTo(contextualMapper.readFileContent("hal-explicit-and-implicit-relations.json"));
}
@Test // #175 #864
@@ -306,7 +306,7 @@ public class HalModelBuilderUnitTest {
}
assertThat(this.mapper.writeValueAsString(builder.build()))
.isEqualTo(contextualMapper.readFile("zoom-hypermedia.json"));
.isEqualTo(contextualMapper.readFileContent("zoom-hypermedia.json"));
}
@Test // #864

View File

@@ -63,7 +63,7 @@ class HalObjectMapperCustomizerTest {
void objectMapperCustomizerShouldBeApplied() throws Exception {
String actualHalJson = this.mockMvc.perform(get("/employees/0")).andReturn().getResponse().getContentAsString();
String expectedHalJson = this.mapper.readFile("hal-custom.json");
String expectedHalJson = this.mapper.readFileContent("hal-custom.json");
assertThat(actualHalJson).isEqualTo(expectedHalJson);
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.hateoas.mediatype.hal;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.MappingTestUtils;
import org.springframework.hateoas.mediatype.MessageResolver;
@@ -59,7 +60,8 @@ public class HalTestUtils {
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, configuration));
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, configuration,
new DefaultListableBeanFactory()));
return mapper;
}

View File

@@ -16,25 +16,28 @@
package org.springframework.hateoas.mediatype.hal.forms;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MappingTestUtils.ContextualMapper;
import org.springframework.hateoas.client.LinkDiscoverer;
import org.springframework.hateoas.client.LinkDiscovererUnitTest;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Unit tests for {@link HalFormsLinkDiscoverer}.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest {
static final LinkDiscoverer discoverer = new HalFormsLinkDiscoverer();
static final ContextualMapper mapper = ContextualMapper.of(HalFormsLinkDiscovererUnitTest.class, new ObjectMapper());
/**
* @see #314
@@ -50,7 +53,7 @@ class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest {
@Test
void discoversAllTheLinkAttributes() throws IOException {
String linkText = read(new ClassPathResource("hal-forms-link.json", getClass()));
String linkText = mapper.readFileContent("hal-forms-link.json");
Link expected = Link.valueOf("</customer/1>;" //
+ "rel=\"self\";" //
@@ -73,12 +76,7 @@ class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest {
@Override
protected String getInputString() {
try {
return read(new ClassPathResource("hal-forms-link-discoverer.json", getClass()));
} catch (IOException e) {
throw new RuntimeException(e);
}
return mapper.readFileContent("hal-forms-link-discoverer.json");
}
@Override

View File

@@ -1,140 +0,0 @@
/*
* Copyright 2017-2021 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
*
* https://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.mediatype.hal.forms;
import static org.assertj.core.api.Assertions.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Greg Turnquist
* @author Oliver Gierke
*/
class HalFormsMessageConverterUnitTest {
ObjectMapper mapper;
HttpMessageConverter<Object> messageConverter;
@BeforeEach
void setUp() {
this.mapper = new ObjectMapper();
this.mapper.registerModule(new Jackson2HalFormsModule());
this.mapper.setHandlerInstantiator(
new Jackson2HalFormsModule.HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(), new DefaultListableBeanFactory()));
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
RepresentationModel.class);
converter.setObjectMapper(mapper);
this.messageConverter = converter;
}
@Test
void canReadAHalFormsDocumentMessage() throws IOException {
HttpInputMessage message = new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return new ClassPathResource("reference.json", getClass()).getInputStream();
}
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
};
Object convertedMessage = this.messageConverter.read(HalFormsDocument.class, message);
assertThat(convertedMessage).isInstanceOf(HalFormsDocument.class);
HalFormsDocument<?> halFormsDocument = (HalFormsDocument<?>) convertedMessage;
assertThat(halFormsDocument.getLinks()).hasSize(2);
assertThat(halFormsDocument.getLinks()).extracting(Link::getHref).containsExactly("/employees", "/employees/1");
assertThat(halFormsDocument.getTemplates().size()).isEqualTo(1);
assertThat(halFormsDocument.getTemplates().keySet()).containsExactly("default");
assertThat(halFormsDocument.getTemplates().get("default").getContentType()).isEqualTo("application/hal+json");
assertThat(halFormsDocument.getTemplates().get("default").getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(halFormsDocument.getTemplates().get("default").getMethod())
.isEqualTo(HttpMethod.GET.toString().toLowerCase());
}
@Test
@SuppressWarnings("rawtypes")
void canWriteAHalFormsDocumentMessage() throws IOException {
HalFormsProperty property = HalFormsProperty.named("my-name")//
.withReadOnly(true) //
.withValue("my-value") //
.withPrompt("my-prompt") //
.withRegex("my-regex") //
.withRequired(true);
HalFormsTemplate template = HalFormsTemplate.forMethod(HttpMethod.GET) //
.withTitle("HAL-FORMS unit test") //
.withContentType(MediaTypes.HAL_JSON) //
.andProperty(property); //
HalFormsDocument expected = HalFormsDocument.empty() //
.andLink(Link.of("/employees").withRel("collection")) //
.andLink(Link.of("/employees/1").withSelfRel())//
.andTemplate("foo", template);
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
HttpOutputMessage convertedMessage = new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return stream;
}
@Override
public HttpHeaders getHeaders() {
return new HttpHeaders();
}
};
this.messageConverter.write(expected, MediaTypes.HAL_FORMS_JSON, convertedMessage);
assertThat(this.mapper.readValue(stream.toString(), HalFormsDocument.class)).isEqualTo(expected);
}
}

View File

@@ -64,7 +64,7 @@ class HalFormsObjectMapperCustomizerTest {
String actualHalFormsJson = mockMvc.perform(get("/employees/0")).andReturn().getResponse()
.getContentAsString();
String expectedHalFormsJson = mapper.readFile("hal-forms-custom.json");
String expectedHalFormsJson = mapper.readFileContent("hal-forms-custom.json");
assertThat(actualHalFormsJson).isEqualTo(expectedHalFormsJson);
}

View File

@@ -29,26 +29,20 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MappingTestUtils;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.support.WebMvcEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Greg Turnquist
*/
@@ -121,7 +115,7 @@ class HalFormsWebMvcIntegrationTest {
@Test
void createNewEmployee() throws Exception {
String specBasedJson = MappingTestUtils.createMapper(getClass()).readFile("new-employee.json");
String specBasedJson = MappingTestUtils.createMapper(getClass()).readFileContent("new-employee.json");
this.mockMvc.perform(post("/employees") //
.content(specBasedJson) //
@@ -145,18 +139,9 @@ class HalFormsWebMvcIntegrationTest {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass)) {
HalFormsMediaTypeConfiguration mediaTypeConfiguration = context.getBean(HalFormsMediaTypeConfiguration.class);
ObjectMapper mapper = mediaTypeConfiguration.configureObjectMapper(new ObjectMapper());
HalFormsConfiguration actual = mediaTypeConfiguration.getResolvedConfiguration();
assertThatCode(() -> {
JsonSerializer<Object> serializer = mapper.getSerializerProviderInstance() //
.findValueSerializer(Links.class);
assertThat(serializer).isInstanceOfSatisfying(HalLinkListSerializer.class, it -> {
assertThat(ReflectionTestUtils.getField(serializer, "halConfiguration")).isSameAs(configuration);
});
}).doesNotThrowAnyException();
assertThat(actual.getHalConfiguration()).isSameAs(configuration);
}
}

View File

@@ -27,6 +27,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@@ -37,36 +38,40 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MappingTestUtils;
import org.springframework.hateoas.MappingTestUtils.ContextualMapper;
import org.springframework.hateoas.PagedModel;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.config.HateoasConfiguration;
import org.springframework.hateoas.mediatype.Affordances;
import org.springframework.hateoas.mediatype.MessageResolver;
import org.springframework.hateoas.mediatype.hal.CurieProvider;
import org.springframework.hateoas.mediatype.hal.DefaultCurieProvider;
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
import org.springframework.hateoas.mediatype.hal.HalTestUtils;
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
import org.springframework.hateoas.mediatype.hal.SimpleAnnotatedPojo;
import org.springframework.hateoas.mediatype.hal.SimplePojo;
import org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider;
import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider;
import org.springframework.hateoas.server.core.EmbeddedWrappers;
import org.springframework.hateoas.support.EmployeeResource;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -77,26 +82,34 @@ import com.jayway.jsonpath.PathNotFoundException;
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {
class Jackson2HalFormsIntegrationTest {
static final Links PAGINATION_LINKS = Links.of( //
Link.of("foo", IanaLinkRelations.NEXT), //
Link.of("bar", IanaLinkRelations.PREV) //
);
final LinkRelationProvider provider = new DelegatingLinkRelationProvider(new AnnotationLinkRelationProvider(),
HalTestUtils.DefaultLinkRelationProvider.INSTANCE);
final Consumer<ObjectMapper> configurer = it -> {
it.registerModule(new Jackson2HalFormsModule());
it.configure(SerializationFeature.INDENT_OUTPUT, true);
};
final ContextualMapper mapper = MappingTestUtils.createMapper(Jackson2HalFormsIntegrationTest.class,
configurer.andThen(it -> {
GenericApplicationContext context = new AnnotationConfigApplicationContext(
HalFormsMediaTypeConfiguration.class, HateoasConfiguration.class);
it.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider, CurieProvider.NONE,
MessageResolver.DEFAULTS_ONLY, new HalConfiguration(), context.getAutowireCapableBeanFactory()));
}));
@BeforeEach
void setUpModule() {
LocaleContextHolder.setLocale(Locale.US);
LinkRelationProvider provider = new DelegatingLinkRelationProvider(new AnnotationLinkRelationProvider(),
HalTestUtils.DefaultLinkRelationProvider.INSTANCE);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( //
provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(),
new DefaultListableBeanFactory()));
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
@Test
@@ -105,8 +118,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
RepresentationModel<?> resourceSupport = new RepresentationModel<>();
resourceSupport.add(Link.of("localhost"));
assertThat(write(resourceSupport))
.isEqualTo(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())));
assertThat(mapper.writeObject(resourceSupport))
.isEqualTo(mapper.readFileContent("single-link-reference.json"));
}
@Test
@@ -115,8 +128,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
RepresentationModel<?> expected = new RepresentationModel<>();
expected.add(Link.of("localhost"));
assertThat(read(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())),
RepresentationModel.class)).isEqualTo(expected);
assertThat(mapper.readFile("single-link-reference.json")).isEqualTo(expected);
}
@Test
@@ -126,8 +138,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
resourceSupport.add(Link.of("localhost"));
resourceSupport.add(Link.of("localhost2"));
assertThat(write(resourceSupport))
.isEqualTo(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())));
assertThat(mapper.writeObject(resourceSupport))
.isEqualTo(mapper.readFileContent("list-link-reference.json"));
}
@Test
@@ -137,8 +149,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
expected.add(Link.of("localhost"));
expected.add(Link.of("localhost2"));
assertThat(read(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())),
RepresentationModel.class)).isEqualTo(expected);
assertThat(mapper.readFile("list-link-reference.json", RepresentationModel.class)).isEqualTo(expected);
}
@Test
@@ -154,8 +165,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
resource.add(link);
assertThat(write(resource))
.isEqualTo(MappingUtils.read(new ClassPathResource("employee-resource-support.json", getClass())));
assertThat(mapper.writeObject(resource))
.isEqualTo(mapper.readFileContent("employee-resource-support.json"));
}
@Test
@@ -163,20 +174,17 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
EntityModel<SimplePojo> resource = EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost"));
assertThat(write(resource))
.isEqualTo(MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())));
assertThat(mapper.writeObject(resource))
.isEqualTo(mapper.readFileContent("simple-resource-unwrapped.json"));
}
@Test
void deserializesResource() throws IOException {
EntityModel<SimplePojo> expected = EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost"));
EntityModel<SimplePojo> result = mapper.readFile("simple-resource-unwrapped.json", EntityModel.class,
SimplePojo.class);
EntityModel<SimplePojo> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())),
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class));
assertThat(result).isEqualTo(expected);
assertThat(result).isEqualTo(EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost")));
}
@Test
@@ -189,8 +197,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<String> resources = CollectionModel.of(content);
resources.add(Link.of("localhost"));
assertThat(write(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())));
assertThat(mapper.writeObject(resources))
.isEqualTo(mapper.readFileContent("simple-embedded-resource-reference.json"));
}
@Test
@@ -203,9 +211,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<String> expected = CollectionModel.of(content);
expected.add(Link.of("localhost"));
CollectionModel<String> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())),
mapper.getTypeFactory().constructParametricType(CollectionModel.class, String.class));
CollectionModel<String> result = mapper.readFile("simple-embedded-resource-reference.json", CollectionModel.class,
String.class);
assertThat(result).isEqualTo(expected);
@@ -220,8 +227,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimplePojo>> resources = CollectionModel.of(content);
resources.add(Link.of("localhost"));
assertThat(write(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())));
assertThat(mapper.writeObject(resources))
.isEqualTo(mapper.readFileContent("single-embedded-resource-reference.json"));
}
@Test
@@ -233,10 +240,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimplePojo>> expected = CollectionModel.of(content);
expected.add(Link.of("localhost"));
CollectionModel<EntityModel<SimplePojo>> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())),
mapper.getTypeFactory().constructParametricType(CollectionModel.class,
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class)));
CollectionModel<EntityModel<SimplePojo>> result = mapper.readFile("single-embedded-resource-reference.json",
CollectionModel.class, EntityModel.class, SimplePojo.class);
assertThat(result).isEqualTo(expected);
}
@@ -247,8 +252,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimplePojo>> resources = setupResources();
resources.add(Link.of("localhost"));
assertThat(write(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())));
assertThat(mapper.writeObject(resources))
.isEqualTo(mapper.readFileContent("multiple-resource-resources.json"));
}
@Test
@@ -257,10 +262,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimplePojo>> expected = setupResources();
expected.add(Link.of("localhost"));
CollectionModel<EntityModel<SimplePojo>> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())),
mapper.getTypeFactory().constructParametricType(CollectionModel.class,
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class)));
CollectionModel<EntityModel<SimplePojo>> result = mapper.readFile("multiple-resource-resources.json",
CollectionModel.class, EntityModel.class, SimplePojo.class);
assertThat(result).isEqualTo(expected);
}
@@ -274,8 +277,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimpleAnnotatedPojo>> resources = CollectionModel.of(content);
resources.add(Link.of("localhost"));
assertThat(write(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("annotated-resource-resources.json"));
}
@Test
@@ -287,43 +289,43 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<EntityModel<SimpleAnnotatedPojo>> expected = CollectionModel.of(content);
expected.add(Link.of("localhost"));
CollectionModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())),
mapper.getTypeFactory().constructParametricType(CollectionModel.class,
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class)));
CollectionModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readFile("annotated-resource-resources.json",
CollectionModel.class, EntityModel.class, SimpleAnnotatedPojo.class);
assertThat(result).isEqualTo(expected);
}
@Test
void serializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception {
assertThat(write(setupAnnotatedResources()))
.isEqualTo(MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())));
assertThat(mapper.writeObject(setupAnnotatedResources()))
.isEqualTo(mapper.readFileContent("annotated-embedded-resources-reference.json"));
}
@Test
void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception {
CollectionModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())),
mapper.getTypeFactory().constructParametricType(CollectionModel.class,
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class)));
mapper.readFile("annotated-embedded-resources-reference.json",
CollectionModel.class, EntityModel.class, SimpleAnnotatedPojo.class);
CollectionModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readFile(
"annotated-embedded-resources-reference.json", CollectionModel.class, EntityModel.class,
SimpleAnnotatedPojo.class);
assertThat(result).isEqualTo(setupAnnotatedResources());
}
@Test
void serializesPagedResource() throws Exception {
assertThat(write(setupAnnotatedPagedResources()))
.isEqualTo(MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())));
assertThat(mapper.writeObject(setupAnnotatedPagedResources()))
.isEqualTo(mapper.readFileContent("annotated-paged-resources.json"));
}
@Test
void deserializesPagedResource() throws Exception {
PagedModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readValue(
MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())),
mapper.getTypeFactory().constructParametricType(PagedModel.class,
mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class)));
PagedModel<EntityModel<SimpleAnnotatedPojo>> result = mapper.readFile("annotated-paged-resources.json",
PagedModel.class, EntityModel.class, SimpleAnnotatedPojo.class);
assertThat(result).isEqualTo(setupAnnotatedPagedResources());
}
@@ -334,16 +336,16 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<Object> resources = CollectionModel.of(Collections.emptySet(), Link.of("foo"),
Link.of("bar", "myrel"));
assertThat(getCuriedObjectMapper().writeValueAsString(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("curied-document.json", getClass())));
assertThat(getCuriedObjectMapper().writeObject(resources))
.isEqualTo(mapper.readFileContent("curied-document.json"));
}
@Test
void doesNotRenderCuriesIfNoLinkIsPresent() throws Exception {
CollectionModel<Object> resources = CollectionModel.of(Collections.emptySet());
assertThat(getCuriedObjectMapper().writeValueAsString(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("empty-document.json", getClass())));
assertThat(getCuriedObjectMapper().writeObject(resources))
.isEqualTo(mapper.readFileContent("empty-document.json"));
}
@Test
@@ -352,8 +354,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<Object> resources = CollectionModel.of(Collections.emptySet());
resources.add(Link.of("foo"));
assertThat(getCuriedObjectMapper().writeValueAsString(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("single-non-curie-document.json", getClass())));
assertThat(getCuriedObjectMapper().writeObject(resources))
.isEqualTo(mapper.readFileContent("single-non-curie-document.json"));
}
@Test
@@ -362,7 +364,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
RepresentationModel<?> support = new RepresentationModel<>();
support.add(Link.of("/foo{?bar}", "search"));
assertThat(write(support)).isEqualTo(MappingUtils.read(new ClassPathResource("link-template.json", getClass())));
assertThat(mapper.writeObject(support)).isEqualTo(mapper.readFileContent("link-template.json"));
}
@Test
@@ -378,8 +380,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
}
};
assertThat(getCuriedObjectMapper(provider).writeValueAsString(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("multiple-curies-document.json", getClass())));
assertThat(getCuriedObjectMapper(provider).writeObject(resources))
.isEqualTo(mapper.readFileContent("multiple-curies-document.json"));
}
@Test
@@ -392,8 +394,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
CollectionModel<Object> resources = CollectionModel.of(values);
assertThat(write(resources))
.isEqualTo(MappingUtils.read(new ClassPathResource("empty-embedded-pojos.json", getClass())));
assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("empty-embedded-pojos.json"));
}
@Test
@@ -415,13 +416,13 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
RepresentationModel<?> original = new RepresentationModel<>();
original.add(Link.of("/orders{?id}", "order"));
String serialized = mapper.writeValueAsString(original);
String serialized = mapper.writeObject(original);
String expected = "{\n \"_links\" : {\n \"order\" : {\n \"href\" : \"/orders{?id}\",\n \"templated\" : true\n }\n }\n}";
assertThat(serialized).isEqualTo(expected);
RepresentationModel<?> deserialized = mapper.readValue(serialized, RepresentationModel.class);
RepresentationModel<?> deserialized = mapper.readObject(serialized);
assertThat(deserialized).isEqualTo(original);
}
@@ -443,12 +444,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
.toLink();
EntityModel<HalFormsPayload> model = EntityModel.of(new HalFormsPayload(), link);
ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
assertThatCode(() -> {
String promptString = JsonPath.compile("$._templates.default.properties[0].prompt") //
.read(mapper.writeValueAsString(model));
.read(mapper.writeObject(model));
assertThat(promptString).isEqualTo("Vorname");
@@ -474,12 +475,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
.toLink();
EntityModel<HalFormsPayload> model = EntityModel.of(new HalFormsPayload(), link);
ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
assertThatCode(() -> {
String promptString = JsonPath.compile("$._templates.default.title") //
.read(mapper.writeValueAsString(model));
.read(mapper.writeObject(model));
assertThat(promptString).isEqualTo("Template title");
@@ -545,12 +546,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
.toLink();
EntityModel<HalFormsPayload> model = EntityModel.of(new HalFormsPayload(), link);
ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source);
assertThatCode(() -> {
String promptString = JsonPath.compile("$._templates.default.properties[0].placeholder") //
.read(mapper.writeValueAsString(model));
.read(mapper.writeObject(model));
assertThat(promptString).isEqualTo("Property placeholder");
@@ -559,8 +560,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception {
ObjectMapper mapper = getCuriedObjectMapper();
String json = mapper.writeValueAsString(toMarshall);
String json = getCuriedObjectMapper().writeObject(toMarshall);
assertThatExceptionOfType(PathNotFoundException.class) //
.isThrownBy(() -> JsonPath.compile(path).read(json));
@@ -568,8 +568,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
private void assertValueForPath(Object toMarshall, String path, Object expected) throws Exception {
ObjectMapper mapper = getCuriedObjectMapper();
String json = mapper.writeValueAsString(toMarshall);
String json = getCuriedObjectMapper().writeObject(toMarshall);
Object actual = JsonPath.compile(path).read(json);
@@ -585,13 +584,13 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(resourceBundleKey, Locale.US, "Foobar's title!");
ObjectMapper objectMapper = getCuriedObjectMapper(CurieProvider.NONE, messageSource);
ContextualMapper objectMapper = getCuriedObjectMapper(CurieProvider.NONE, messageSource);
RepresentationModel<?> resource = new RepresentationModel<>();
resource.add(Link.of("target", "ns:foobar"));
assertThat(objectMapper.writeValueAsString(resource))
.isEqualTo(MappingUtils.read(new ClassPathResource("link-with-title.json", getClass())));
assertThat(objectMapper.writeObject(resource))
.isEqualTo(objectMapper.readFileContent("link-with-title.json"));
}
private static CollectionModel<EntityModel<SimplePojo>> setupResources() {
@@ -621,32 +620,34 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
return PagedModel.of(content, new PagedModel.PageMetadata(2, 0, 4), PAGINATION_LINKS);
}
private ObjectMapper getCuriedObjectMapper() {
private ContextualMapper getCuriedObjectMapper() {
return getCuriedObjectMapper(new DefaultCurieProvider("foo", UriTemplate.of("http://localhost:8080/rels/{rel}")));
}
private ObjectMapper getCuriedObjectMapper(CurieProvider provider) {
private ContextualMapper getCuriedObjectMapper(CurieProvider provider) {
return getCuriedObjectMapper(provider, null);
}
private ObjectMapper getCuriedObjectMapper(CurieProvider provider, @Nullable MessageSource messageSource) {
private ContextualMapper getCuriedObjectMapper(CurieProvider provider, @Nullable MessageSource messageSource) {
ObjectMapper mapper = new ObjectMapper();
MessageResolver resolver = MessageResolver.of(messageSource);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), provider,
MessageResolver.of(messageSource), new HalFormsConfiguration(), new DefaultListableBeanFactory()));
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.setSerializationInclusion(Include.NON_NULL);
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), resolver);
factory.registerSingleton("foobar", new HalFormsTemplatePropertyWriter(builder));
return mapper;
return MappingTestUtils.createMapper(Jackson2HalFormsIntegrationTest.class, configurer.andThen(it -> {
it.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(this.provider, provider,
resolver, new HalConfiguration(), factory));
}));
}
@JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY)
public static class HalFormsPayload {
private @Getter String firstname;
}
@JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY)
public static class Jsr303Sample {
private String firstname;
@@ -671,6 +672,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
}
}
@JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY)
public static class UnwrappedExampleElement {
private @Getter String firstname;
}

View File

@@ -18,13 +18,17 @@ package org.springframework.hateoas.support;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.hateoas.RepresentationModel;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Greg Turnquist
*/
@Data
@Getter(onMethod = @__(@JsonProperty))
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class EmployeeResource extends RepresentationModel<EmployeeResource> {
@@ -32,7 +36,7 @@ public class EmployeeResource extends RepresentationModel<EmployeeResource> {
private String name;
public EmployeeResource(EmployeeResource employeeResource) {
this.name = employeeResource.getName();
add(employeeResource.getLinks());
}

View File

@@ -1,4 +1,5 @@
{
"name" : "Frodo Baggins",
"_links" : {
"self" : {
"href" : "/employees/1"
@@ -12,6 +13,5 @@
"type" : "text"
} ]
}
},
"name" : "Frodo Baggins"
}
}