#707 - Refactor Affordances to better support Spring Data REST.
* Move Spring MVC method scanning out of the core Affordances API and into SpringMvcAffordanceBuilder to make it easy to use in Spring Data REST (where method scanning isn't needed). * Also push attributes from Affordance into AffordanceModel to centralize the details. * Certain serializers must be registered differently due to Jackson's rules of precedence given Spring Data REST needs to override them.
This commit is contained in:
@@ -15,33 +15,44 @@
|
||||
*/
|
||||
package org.springframework.hateoas;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Value;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation.
|
||||
* Hold the {@link AffordanceModel}s for all supported media types.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface Affordance {
|
||||
@Value
|
||||
public class Affordance {
|
||||
|
||||
private static List<AffordanceModelFactory> factories = SpringFactoriesLoader.loadFactories(AffordanceModelFactory.class, Affordance.class.getClassLoader());
|
||||
|
||||
/**
|
||||
* HTTP method this affordance covers. For multiple methods, add multiple {@link Affordance}s.
|
||||
*
|
||||
* @return
|
||||
* Collection of {@link AffordanceModel}s related to this affordance.
|
||||
*/
|
||||
HttpMethod getHttpMethod();
|
||||
private final Map<MediaType, AffordanceModel> affordanceModels = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Name for the REST action this {@link Affordance} can take.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getName();
|
||||
public Affordance(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
|
||||
Assert.notNull(httpMethod, "httpMethod must not be null!");
|
||||
Assert.notNull(queryMethodParameters, "queryMethodParameters must not be null!");
|
||||
|
||||
for (AffordanceModelFactory factory : factories) {
|
||||
this.affordanceModels.put(factory.getMediaType(), factory.getAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the {@link AffordanceModel} for the requested {@link MediaType}.
|
||||
@@ -49,18 +60,8 @@ public interface Affordance {
|
||||
* @param mediaType
|
||||
* @return
|
||||
*/
|
||||
<T extends AffordanceModel> T getAffordanceModel(MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<MethodParameter> getInputMethodParameters();
|
||||
|
||||
/**
|
||||
* Get a listing of {@link QueryParameter}s.
|
||||
* @return
|
||||
*/
|
||||
List<QueryParameter> getQueryMethodParameters();
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AffordanceModel> T getAffordanceModel(MediaType mediaType) {
|
||||
return (T) this.affordanceModels.get(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2018 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.
|
||||
@@ -15,23 +15,61 @@
|
||||
*/
|
||||
package org.springframework.hateoas;
|
||||
|
||||
import java.util.Collection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* An affordance model is a media type specific description of an affordance.
|
||||
* Collection of attributes needed to render any form of hypermedia.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface AffordanceModel {
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public abstract class AffordanceModel {
|
||||
|
||||
/**
|
||||
* The media types this is a model for. Can be multiple ones as often media types come in different flavors like an
|
||||
* XML and JSON one and in simple cases a single model might serve them all.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* Name for the REST action of this resource.
|
||||
*/
|
||||
Collection<MediaType> getMediaTypes();
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* {@link Link} for the URI of the resource.
|
||||
*/
|
||||
private Link link;
|
||||
|
||||
/**
|
||||
* Request method verb for this resource. For multiple methods, add multiple {@link Affordance}s.
|
||||
*/
|
||||
private HttpMethod httpMethod;
|
||||
|
||||
/**
|
||||
* Domain type used to create a new resource.
|
||||
*/
|
||||
private ResolvableType inputType;
|
||||
|
||||
/**
|
||||
* Collection of {@link QueryParameter}s to interrogate a resource.
|
||||
*/
|
||||
private List<QueryParameter> queryMethodParameters;
|
||||
|
||||
/**
|
||||
* Response body domain type.
|
||||
*/
|
||||
private ResolvableType outputType;
|
||||
|
||||
/**
|
||||
* Expand the {@link Link} into an {@literal href} with no parameters.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getURI() {
|
||||
return this.link.expand().getHref();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -189,6 +191,48 @@ public class Link implements Serializable {
|
||||
return withAffordances(newAffordances);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method when chaining an existing {@link Link}.
|
||||
*
|
||||
* @param name
|
||||
* @param httpMethod
|
||||
* @param inputType
|
||||
* @param queryMethodParameters
|
||||
* @param outputType
|
||||
* @return
|
||||
*/
|
||||
public Link andAffordance(String name, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return andAffordance(new Affordance(name, this, httpMethod, inputType, queryMethodParameters, outputType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname, e.g.
|
||||
* {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
|
||||
*
|
||||
* @param httpMethod
|
||||
* @param inputType
|
||||
* @param queryMethodParameters
|
||||
* @param outputType
|
||||
* @return
|
||||
*/
|
||||
public Link andAffordance(HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return andAffordance(httpMethod.toString().toLowerCase() + inputType.resolve().getSimpleName(), httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname, e.g.
|
||||
* {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
|
||||
*
|
||||
* @param httpMethod
|
||||
* @param inputType
|
||||
* @param queryMethodParameters
|
||||
* @param outputType
|
||||
* @return
|
||||
*/
|
||||
public Link andAffordance(HttpMethod httpMethod, Class<?> inputType, List<QueryParameter> queryMethodParameters, Class<?> outputType) {
|
||||
return andAffordance(httpMethod, ResolvableType.forClass(inputType), queryMethodParameters, ResolvableType.forClass(outputType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link Link} with additional {@link Affordance}s.
|
||||
*
|
||||
|
||||
@@ -19,7 +19,7 @@ import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Web framework-neutral representation of a web request's query parameter (http://example.com?name=foo).
|
||||
* Representation of a web request's query parameter (http://example.com?name=foo) => {"name", "foo", true}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@@ -28,6 +28,6 @@ import lombok.RequiredArgsConstructor;
|
||||
public class QueryParameter {
|
||||
|
||||
private final String name;
|
||||
private final boolean required;
|
||||
private final String value;
|
||||
private final boolean required;
|
||||
}
|
||||
|
||||
@@ -15,98 +15,78 @@
|
||||
*/
|
||||
package org.springframework.hateoas.collectionjson;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class CollectionJsonAffordanceModel implements AffordanceModel {
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CollectionJsonAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
|
||||
HttpMethod.PATCH);
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
|
||||
|
||||
private final Affordance affordance;
|
||||
private final UriComponents components;
|
||||
private final @Getter List<CollectionJsonData> inputProperties;
|
||||
private final @Getter List<CollectionJsonData> queryProperties;
|
||||
|
||||
public CollectionJsonAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
|
||||
CollectionJsonAffordanceModel(Affordance affordance, UriComponents components) {
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
|
||||
this.affordance = affordance;
|
||||
this.components = components;
|
||||
|
||||
this.inputProperties = determineAffordanceInputs();
|
||||
this.inputProperties = determineInputs();
|
||||
this.queryProperties = determineQueryProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<MediaType> getMediaTypes() {
|
||||
return Collections.singleton(MediaTypes.COLLECTION_JSON);
|
||||
}
|
||||
/**
|
||||
* Look at the input's domain type to extract the {@link Affordance}'s properties.
|
||||
* Then transform them into a list of {@link CollectionJsonData} objects.
|
||||
*/
|
||||
private List<CollectionJsonData> determineInputs() {
|
||||
|
||||
public String getRel() {
|
||||
return isHttpGetMethod() ? this.affordance.getName() : "";
|
||||
}
|
||||
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
|
||||
public String getUri() {
|
||||
return isHttpGetMethod() ? this.components.toUriString() : "";
|
||||
return PropertyUtils.findPropertyNames(getInputType()).stream()
|
||||
.map(propertyName -> new CollectionJsonData()
|
||||
.withName(propertyName)
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a list of {@link QueryParameter}s into a list of {@link CollectionJsonData} objects.
|
||||
*
|
||||
* Transform a list of general {@link QueryParameter}s into a list of {@link CollectionJsonData} objects.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private List<CollectionJsonData> determineQueryProperties() {
|
||||
|
||||
if (!isHttpGetMethod()) {
|
||||
if (getHttpMethod().equals(HttpMethod.GET)) {
|
||||
|
||||
return getQueryMethodParameters().stream()
|
||||
.map(queryProperty -> new CollectionJsonData()
|
||||
.withName(queryProperty.getName())
|
||||
.withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return this.affordance.getQueryMethodParameters().stream()
|
||||
.map(queryProperty -> new CollectionJsonData().withName(queryProperty.getName()).withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean isHttpGetMethod() {
|
||||
return this.affordance.getHttpMethod().equals(HttpMethod.GET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
|
||||
* Then transform them into a list of {@link CollectionJsonData} objects.
|
||||
*/
|
||||
private List<CollectionJsonData> determineAffordanceInputs() {
|
||||
|
||||
if (!METHODS_FOR_INPUT_DETECTION.contains(affordance.getHttpMethod())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return this.affordance.getInputMethodParameters().stream()
|
||||
.findFirst()
|
||||
.map(methodParameter -> {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
return PropertyUtils.findProperties(resolvableType);
|
||||
})
|
||||
.orElse(Collections.emptyList())
|
||||
.stream()
|
||||
.map(property -> new CollectionJsonData().withName(property).withValue(""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,31 +17,28 @@ package org.springframework.hateoas.collectionjson;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* Factory for creating {@link CollectionJsonAffordanceModel}s.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class CollectionJsonAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.COLLECTION_JSON;
|
||||
|
||||
/**
|
||||
* Look up the {@link AffordanceModel} for this factory.
|
||||
*
|
||||
* @param affordance
|
||||
* @param invocationValue
|
||||
* @param components
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
|
||||
return new CollectionJsonAffordanceModel(affordance, components);
|
||||
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return new CollectionJsonAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.hateoas.collectionjson;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
@@ -29,7 +28,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@Wither(AccessLevel.PACKAGE)
|
||||
@Wither
|
||||
@JsonIgnoreProperties()
|
||||
class CollectionJsonData {
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ public class Jackson2CollectionJsonModule extends SimpleModule {
|
||||
setMixInAnnotation(Resource.class, ResourceMixin.class);
|
||||
setMixInAnnotation(Resources.class, ResourcesMixin.class);
|
||||
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
|
||||
|
||||
addSerializer(new CollectionJsonPagedResourcesSerializer());
|
||||
addSerializer(new CollectionJsonResourcesSerializer());
|
||||
addSerializer(new CollectionJsonResourceSerializer());
|
||||
addSerializer(new CollectionJsonResourceSupportSerializer());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -853,11 +858,12 @@ public class Jackson2CollectionJsonModule extends SimpleModule {
|
||||
/**
|
||||
* For Collection+JSON, "queries" are only collected for GET affordances where the URI is NOT a self link.
|
||||
*/
|
||||
if (affordance.getHttpMethod().equals(HttpMethod.GET) && !model.getUri().equals(selfLink.getHref())) {
|
||||
if (model.getHttpMethod() == HttpMethod.GET && !model.getURI().equals(selfLink.getHref())) {
|
||||
|
||||
|
||||
queries.add(new CollectionJsonQuery()
|
||||
.withRel(model.getRel())
|
||||
.withHref(model.getUri())
|
||||
.withRel(model.getName())
|
||||
.withHref(model.getURI())
|
||||
.withData(model.getQueryProperties()));
|
||||
}
|
||||
});
|
||||
@@ -883,7 +889,7 @@ public class Jackson2CollectionJsonModule extends SimpleModule {
|
||||
/**
|
||||
* For Collection+JSON, "templates" are made of any non-GET affordances.
|
||||
*/
|
||||
if (!affordance.getHttpMethod().equals(HttpMethod.GET)) {
|
||||
if (!(model.getHttpMethod() == HttpMethod.GET)) {
|
||||
|
||||
CollectionJsonTemplate template = new CollectionJsonTemplate() //
|
||||
.withData(model.getInputProperties());
|
||||
|
||||
@@ -20,14 +20,12 @@ import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonM
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to handle {@link PagedResources}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonPagedResourcesSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonPagedResourcesDeserializer.class)
|
||||
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
|
||||
|
||||
|
||||
@@ -20,14 +20,12 @@ import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonM
|
||||
import org.springframework.hateoas.Resource;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to invoke the related serializer/deserizer.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourceSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourceDeserializer.class)
|
||||
abstract class ResourceMixin<T> extends Resource<T> {
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
* @author Greg Turnquist
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourceSupportSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourceSupportDeserializer.class)
|
||||
abstract class ResourceSupportMixin extends ResourceSupport {
|
||||
|
||||
|
||||
@@ -20,14 +20,12 @@ import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonM
|
||||
import org.springframework.hateoas.Resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* Jackson 2 mixin to invoke the related serializer/deserizer.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@JsonSerialize(using = CollectionJsonResourcesSerializer.class)
|
||||
@JsonDeserialize(using = CollectionJsonResourcesDeserializer.class)
|
||||
abstract class ResourcesMixin<T> extends Resources<T> {
|
||||
|
||||
|
||||
@@ -32,16 +32,19 @@ import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.hateoas.RelProvider;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
|
||||
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.core.DelegatingRelProvider;
|
||||
import org.springframework.hateoas.hal.CurieProvider;
|
||||
import org.springframework.hateoas.hal.HalConfiguration;
|
||||
import org.springframework.hateoas.hal.Jackson2HalModule;
|
||||
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
|
||||
import org.springframework.hateoas.hal.Jackson2HalModule.*;
|
||||
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
|
||||
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
|
||||
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@@ -50,6 +53,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@@ -89,32 +93,34 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
@Override
|
||||
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
|
||||
for (HttpMessageConverter<?> converter : converters) {
|
||||
if (converter instanceof MappingJackson2HttpMessageConverter) {
|
||||
MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter;
|
||||
ObjectMapper objectMapper = halConverterCandidate.getObjectMapper();
|
||||
if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (converters.stream()
|
||||
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
|
||||
.map(AbstractJackson2HttpMessageConverter.class::cast)
|
||||
.map(converter -> converter.getObjectMapper())
|
||||
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectMapper objectMapper = mapper.getIfAvailable(() -> new ObjectMapper());
|
||||
|
||||
CurieProvider curieProvider = this.curieProvider.getIfAvailable();
|
||||
RelProvider relProvider = this.relProvider.getObject();
|
||||
|
||||
ObjectMapper objectMapper = mapper.getIfAvailable(ObjectMapper::new);
|
||||
|
||||
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
|
||||
MessageSourceAccessor.class);
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.HAL)) {
|
||||
converters.add(0, createHalConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
|
||||
}
|
||||
if (hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
|
||||
converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
|
||||
}
|
||||
CurieProvider curieProvider = this.curieProvider.getIfAvailable();
|
||||
RelProvider relProvider = this.relProvider.getObject();
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.HAL)) {
|
||||
converters.add(0, createHalConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
|
||||
}
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
|
||||
converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
|
||||
}
|
||||
}
|
||||
|
||||
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
|
||||
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
|
||||
}
|
||||
@@ -127,18 +133,15 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
*/
|
||||
protected MappingJackson2HttpMessageConverter createCollectionJsonConverter(ObjectMapper objectMapper,
|
||||
MessageSourceAccessor linkRelationMessageSource) {
|
||||
ObjectMapper collectionJsonObjectMapper = objectMapper.copy();
|
||||
|
||||
collectionJsonObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
collectionJsonObjectMapper.registerModule(new Jackson2CollectionJsonModule());
|
||||
collectionJsonObjectMapper.setHandlerInstantiator(
|
||||
new Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator(linkRelationMessageSource));
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
|
||||
MappingJackson2HttpMessageConverter collectionJsonConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class);
|
||||
collectionJsonConverter.setSupportedMediaTypes(Arrays.asList(COLLECTION_JSON));
|
||||
collectionJsonConverter.setObjectMapper(collectionJsonObjectMapper);
|
||||
return collectionJsonConverter;
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2CollectionJsonModule());
|
||||
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
|
||||
|
||||
return new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class, Arrays.asList(COLLECTION_JSON), mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,21 +154,16 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
private MappingJackson2HttpMessageConverter createHalFormsConverter(ObjectMapper objectMapper,
|
||||
CurieProvider curieProvider, RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
|
||||
|
||||
Jackson2HalFormsModule.HalFormsHandlerInstantiator hi = new Jackson2HalFormsModule.HalFormsHandlerInstantiator(
|
||||
relProvider, curieProvider, linkRelationMessageSource, true,
|
||||
this.halFormsConfiguration.getIfAvailable(() -> new HalFormsConfiguration()));
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2HalFormsModule());
|
||||
mapper.setHandlerInstantiator(hi);
|
||||
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(
|
||||
relProvider, curieProvider, linkRelationMessageSource, true,
|
||||
this.halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new)));
|
||||
|
||||
MappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class);
|
||||
converter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON));
|
||||
converter.setObjectMapper(mapper);
|
||||
|
||||
return converter;
|
||||
return new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class, Arrays.asList(HAL_FORMS_JSON), mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,21 +176,14 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
|
||||
private MappingJackson2HttpMessageConverter createHalConverter(ObjectMapper objectMapper, CurieProvider curieProvider,
|
||||
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
|
||||
|
||||
HalConfiguration halConfiguration = this.halConfiguration.getIfAvailable(() -> new HalConfiguration());
|
||||
|
||||
HalHandlerInstantiator instantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
|
||||
linkRelationMessageSource, halConfiguration);
|
||||
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
mapper.setHandlerInstantiator(instantiator);
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider,
|
||||
linkRelationMessageSource, this.halConfiguration.getIfAvailable(HalConfiguration::new)));
|
||||
|
||||
MappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class);
|
||||
converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON, HAL_JSON_UTF8));
|
||||
converter.setObjectMapper(mapper);
|
||||
|
||||
return converter;
|
||||
return new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
ResourceSupport.class, Arrays.asList(HAL_JSON, HAL_JSON_UTF8), mapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,14 @@ import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.hateoas.EntityLinks;
|
||||
import org.springframework.hateoas.LinkDiscoverer;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
|
||||
@@ -84,5 +87,17 @@ public @interface EnableHypermediaSupport {
|
||||
* @see http://amundsen.com/media-types/collection/format/
|
||||
*/
|
||||
COLLECTION_JSON;
|
||||
|
||||
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
|
||||
|
||||
/**
|
||||
* Is this {@literal HAL} or one of its derivatives?
|
||||
*
|
||||
* @param hypermediaType
|
||||
* @return
|
||||
*/
|
||||
public static boolean isHalBasedMediaType(HypermediaType hypermediaType) {
|
||||
return HAL_BASED_MEDIATYPES.contains(hypermediaType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,19 @@
|
||||
*/
|
||||
package org.springframework.hateoas.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.plugin.core.Plugin;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* TODO: Replace this with an interface and a default implementation of {@link #supports(MediaType)} in Java 8.
|
||||
*
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@@ -44,12 +45,15 @@ public interface AffordanceModelFactory extends Plugin<MediaType> {
|
||||
/**
|
||||
* Look up the {@link AffordanceModel} for this factory.
|
||||
*
|
||||
* @param affordance
|
||||
* @param invocationValue
|
||||
* @param components
|
||||
* @param name
|
||||
* @param link
|
||||
* @param httpMethod
|
||||
* @param inputType
|
||||
* @param queryMethodParameters
|
||||
* @param outputType
|
||||
* @return
|
||||
*/
|
||||
AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components);
|
||||
AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType);
|
||||
|
||||
/**
|
||||
* Returns if a plugin should be invoked according to the given delimiter.
|
||||
@@ -59,6 +63,7 @@ public interface AffordanceModelFactory extends Plugin<MediaType> {
|
||||
*/
|
||||
@Override
|
||||
default boolean supports(MediaType delimiter) {
|
||||
|
||||
return Optional.ofNullable(getMediaType())
|
||||
.map(mediaType -> mediaType.equals(delimiter))
|
||||
.orElse(false);
|
||||
|
||||
@@ -15,108 +15,60 @@
|
||||
*/
|
||||
package org.springframework.hateoas.hal.forms;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.core.MethodParameters;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.support.PropertyUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* {@link AffordanceModel} for a HAL-FORMS {@link org.springframework.http.MediaType}.
|
||||
* {@link AffordanceModel} for a HAL-FORMS {@link MediaType}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class HalFormsAffordanceModel implements AffordanceModel {
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class HalFormsAffordanceModel extends AffordanceModel {
|
||||
|
||||
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
|
||||
HttpMethod.PATCH);
|
||||
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
|
||||
|
||||
private final Affordance affordance;
|
||||
private final UriComponents components;
|
||||
private final boolean required;
|
||||
private final List<String> properties;
|
||||
private final @Getter List<HalFormsProperty> inputProperties;
|
||||
|
||||
public HalFormsAffordanceModel(Affordance affordance, UriComponents components) {
|
||||
public HalFormsAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
|
||||
this.affordance = affordance;
|
||||
this.components = components;
|
||||
this.required = determineRequired(affordance.getHttpMethod());
|
||||
this.properties = METHODS_FOR_INPUT_DETECTTION.contains(affordance.getHttpMethod()) //
|
||||
? determineAffordanceInputs() //
|
||||
: Collections.emptyList();
|
||||
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
|
||||
this.inputProperties = determineInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the details of the REST method's {@link MethodParameters} into
|
||||
* {@link HalFormsProperty}s.
|
||||
*
|
||||
* @return
|
||||
* Look at the input's domain type to extract the {@link Affordance}'s properties.
|
||||
* Then transform them into a list of {@link HalFormsProperty} objects.
|
||||
*/
|
||||
public List<HalFormsProperty> getProperties() {
|
||||
private List<HalFormsProperty> determineInputs() {
|
||||
|
||||
return properties.stream() //
|
||||
.map(name -> HalFormsProperty.named(name).withRequired(required)) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getURI() {
|
||||
return components.toUriString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the affordance is pointing to the same path as the given one.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean hasPath(String path) {
|
||||
|
||||
Assert.notNull(path, "Path must not be null!");
|
||||
|
||||
return getURI().equals(path);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModel#getMediaType()
|
||||
*/
|
||||
@Override
|
||||
public Collection<MediaType> getMediaTypes() {
|
||||
return Collections.singleton(MediaTypes.HAL_FORMS_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the Spring MVC controller's HTTP method, decided whether or not input attributes are required or not.
|
||||
*
|
||||
* @param httpMethod - string representation of an HTTP method, e.g. GET, POST, etc.
|
||||
* @return
|
||||
*/
|
||||
private boolean determineRequired(HttpMethod httpMethod) {
|
||||
return Arrays.asList(HttpMethod.POST, HttpMethod.PUT).contains(httpMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
|
||||
*/
|
||||
private List<String> determineAffordanceInputs() {
|
||||
|
||||
return this.affordance.getInputMethodParameters().stream()
|
||||
.findFirst()
|
||||
.map(methodParameter -> {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
return PropertyUtils.findProperties(resolvableType);
|
||||
})
|
||||
.orElse(Collections.emptyList());
|
||||
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
|
||||
|
||||
return PropertyUtils.findPropertyNames(getInputType()).stream()
|
||||
.map(propertyName -> new HalFormsProperty()
|
||||
.withName(propertyName)
|
||||
.withRequired(Arrays.asList(HttpMethod.POST, HttpMethod.PUT).contains(getHttpMethod())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@ package org.springframework.hateoas.hal.forms;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* Factory for creating {@link HalFormsAffordanceModel}s.
|
||||
@@ -35,13 +38,8 @@ class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
|
||||
|
||||
private final @Getter MediaType mediaType = MediaTypes.HAL_FORMS_JSON;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.Affordance, org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation, org.springframework.web.util.UriComponents)
|
||||
*/
|
||||
@Override
|
||||
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue,
|
||||
UriComponents components) {
|
||||
return new HalFormsAffordanceModel(affordance, components);
|
||||
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
|
||||
return new HalFormsAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import lombok.NonNull;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
|
||||
@@ -37,28 +35,26 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
@Value
|
||||
@Wither
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
|
||||
public class HalFormsProperty {
|
||||
@NoArgsConstructor(force = true)
|
||||
class HalFormsProperty {
|
||||
|
||||
private @Wither(AccessLevel.PRIVATE) @NonNull String name;
|
||||
private @NonNull String name;
|
||||
private Boolean readOnly;
|
||||
private String value;
|
||||
private String prompt;
|
||||
private String regex;
|
||||
private boolean templated;
|
||||
private @JsonInclude(Include.ALWAYS) boolean required;
|
||||
private @JsonInclude boolean required;
|
||||
private boolean multi;
|
||||
|
||||
/**
|
||||
* Creates a new {@link HalFormsProperty} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param name must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
|
||||
public static HalFormsProperty named(String name) {
|
||||
|
||||
Assert.hasText(name, "Property name must not be null or empty!");
|
||||
|
||||
return new HalFormsProperty().withName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ class HalFormsSerializers {
|
||||
.withLinks(value.getLinks()) //
|
||||
.withTemplates(findTemplates(value));
|
||||
|
||||
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
|
||||
provider
|
||||
.findValueSerializer(HalFormsDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,7 +150,9 @@ class HalFormsSerializers {
|
||||
.withTemplates(findTemplates(value));
|
||||
}
|
||||
|
||||
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
|
||||
provider
|
||||
.findValueSerializer(HalFormsDocument.class, property)
|
||||
.serialize(doc, gen, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -186,25 +190,27 @@ class HalFormsSerializers {
|
||||
*/
|
||||
private static Map<String, HalFormsTemplate> findTemplates(ResourceSupport resource) {
|
||||
|
||||
Map<String, HalFormsTemplate> templates = new HashMap<String, HalFormsTemplate>();
|
||||
Map<String, HalFormsTemplate> templates = new HashMap<>();
|
||||
|
||||
if (resource.hasLink(Link.REL_SELF)) {
|
||||
|
||||
|
||||
for (Affordance affordance : resource.getLink(Link.REL_SELF).map(Link::getAffordances)
|
||||
.orElse(Collections.emptyList())) {
|
||||
|
||||
HalFormsAffordanceModel model = affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON);
|
||||
|
||||
if (!affordance.getHttpMethod().equals(HttpMethod.GET)) {
|
||||
if (!(model.getHttpMethod() == HttpMethod.GET)) {
|
||||
|
||||
validate(resource, affordance, model);
|
||||
|
||||
HalFormsTemplate template = HalFormsTemplate.forMethod(affordance.getHttpMethod()) //
|
||||
.withProperties(model.getProperties());
|
||||
HalFormsTemplate template = HalFormsTemplate.forMethod(model.getHttpMethod()) //
|
||||
.withProperties(model.getInputProperties());
|
||||
|
||||
/**
|
||||
* First template in HAL-FORMS is "default".
|
||||
*/
|
||||
templates.put(templates.isEmpty() ? "default" : affordance.getName(), template);
|
||||
templates.put(templates.isEmpty() ? "default" : model.getName(), template);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +228,7 @@ class HalFormsSerializers {
|
||||
private static void validate(ResourceSupport resource, Affordance affordance, HalFormsAffordanceModel model) {
|
||||
|
||||
String affordanceUri = model.getURI();
|
||||
String selfLinkUri = resource.getRequiredLink(Link.REL_SELF).getHref();
|
||||
String selfLinkUri = resource.getRequiredLink(Link.REL_SELF).expand().getHref();
|
||||
|
||||
if (!affordanceUri.equals(selfLinkUri)) {
|
||||
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link "
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
import org.springframework.hateoas.RelProvider;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
import org.springframework.hateoas.Resources;
|
||||
import org.springframework.hateoas.hal.CurieProvider;
|
||||
@@ -74,10 +73,12 @@ public class Jackson2HalFormsModule extends SimpleModule {
|
||||
|
||||
setMixInAnnotation(Link.class, LinkMixin.class);
|
||||
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
|
||||
setMixInAnnotation(Resource.class, ResourceMixin.class);
|
||||
setMixInAnnotation(Resources.class, ResourcesMixin.class);
|
||||
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
|
||||
setMixInAnnotation(MediaType.class, MediaTypeMixin.class);
|
||||
|
||||
addSerializer(new HalFormsResourceSerializer());
|
||||
|
||||
}
|
||||
|
||||
@JsonSerialize(using = HalFormsResourceSerializer.class)
|
||||
@@ -105,7 +106,7 @@ public class Jackson2HalFormsModule extends SimpleModule {
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JsonDeserialize(using = MediaTypeDeserializer.class)
|
||||
static interface MediaTypeMixin {}
|
||||
interface MediaTypeMixin {}
|
||||
|
||||
/**
|
||||
* Create new HAL-FORMS serializers based on the context.
|
||||
|
||||
@@ -72,7 +72,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
|
||||
new AnnotationMappingDiscoverer(RequestMapping.class));
|
||||
private static final ControllerLinkBuilderFactory FACTORY = new ControllerLinkBuilderFactory();
|
||||
private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler();
|
||||
private static final SpringMvcAffordanceBuilder AFFORDANCE_BUILDER;
|
||||
|
||||
static {
|
||||
|
||||
@@ -81,7 +80,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
|
||||
|
||||
PluginRegistry<? extends AffordanceModelFactory, MediaType> MODEL_FACTORIES = OrderAwarePluginRegistry
|
||||
.create(factories);
|
||||
AFFORDANCE_BUILDER = new SpringMvcAffordanceBuilder(MODEL_FACTORIES);
|
||||
}
|
||||
|
||||
private final TemplateVariables variables;
|
||||
@@ -357,15 +355,14 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up {@link Affordance}s and {@link org.springframework.hateoas.AffordanceModel}s based on the
|
||||
* {@link MethodInvocation} and {@link UriComponents}.
|
||||
* Look up {@link Affordance}s based on the {@link MethodInvocation} and {@link UriComponents}.
|
||||
*
|
||||
* @param invocation
|
||||
* @param components
|
||||
* @return
|
||||
*/
|
||||
private static Collection<Affordance> findAffordances(MethodInvocation invocation, UriComponents components) {
|
||||
return AFFORDANCE_BUILDER.create(invocation, DISCOVERER, components);
|
||||
return SpringMvcAffordanceBuilder.create(invocation, DISCOVERER, components);
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -377,7 +374,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
|
||||
public UriTemplate getMappingAsUriTemplate(Class<?> type, Method method) {
|
||||
|
||||
String mapping = delegate.getMapping(type, method);
|
||||
|
||||
return templates.computeIfAbsent(mapping, UriTemplate::new);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.mvc;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.AffordanceModel;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.MethodParameters;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* Spring MVC-based representation of an {@link Affordance}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Value
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class SpringMvcAffordance implements Affordance {
|
||||
|
||||
/**
|
||||
* Request method verb associated with the Spring MVC controller method.
|
||||
*/
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
/**
|
||||
* Handle on the Spring MVC controller {@link Method}.
|
||||
*/
|
||||
private final Method method;
|
||||
private final Map<MediaType, AffordanceModel> affordanceModels;
|
||||
|
||||
/**
|
||||
* Construct a Spring MVC-based {@link Affordance} based on Spring MVC controller method and {@link RequestMethod}.
|
||||
*/
|
||||
public SpringMvcAffordance(HttpMethod httpMethod, Method method) {
|
||||
this(httpMethod, method, new HashMap<MediaType, AffordanceModel>());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.Affordance#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.method.getName();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.Affordance#getAffordanceModel(org.springframework.http.MediaType)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends AffordanceModel> T getAffordanceModel(MediaType mediaType) {
|
||||
return (T) this.affordanceModels.get(mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given {@link AffordanceModel} to the {@link Affordance}.
|
||||
*
|
||||
* @param affordanceModel must not be {@literal null}.
|
||||
*/
|
||||
public void addAffordanceModel(AffordanceModel affordanceModel) {
|
||||
|
||||
Assert.notNull(affordanceModel, "Affordance model must not be null!");
|
||||
|
||||
for (MediaType mediaType : affordanceModel.getMediaTypes()) {
|
||||
this.affordanceModels.put(mediaType, affordanceModel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s based on Spring MVC's {@link RequestBody}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<MethodParameter> getInputMethodParameters() {
|
||||
return new MethodParameters(this.method).getParametersWith(RequestBody.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of {@link MethodParameter}s based on Spring MVC's {@link RequestParam}s.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<QueryParameter> getQueryMethodParameters() {
|
||||
|
||||
MethodParameters parameters = new MethodParameters(this.method);
|
||||
|
||||
return parameters.getParametersWith(RequestParam.class).stream()
|
||||
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class))
|
||||
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.required(), requestParam.value()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -15,33 +15,31 @@
|
||||
*/
|
||||
package org.springframework.hateoas.mvc;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.QueryParameter;
|
||||
import org.springframework.hateoas.core.AffordanceModelFactory;
|
||||
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
|
||||
import org.springframework.hateoas.core.MappingDiscoverer;
|
||||
import org.springframework.hateoas.core.MethodParameters;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.plugin.core.PluginRegistry;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
|
||||
/**
|
||||
* Construct {@link SpringMvcAffordance}s using a collection of {@link AffordanceModelFactory}s.
|
||||
* Extract information needed to assemble an {@link Affordance} from a Spring MVC web method.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class SpringMvcAffordanceBuilder {
|
||||
|
||||
private final @NonNull PluginRegistry<? extends AffordanceModelFactory, MediaType> factories;
|
||||
|
||||
/**
|
||||
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
|
||||
* a set of {@link Affordance}s.
|
||||
@@ -51,21 +49,33 @@ class SpringMvcAffordanceBuilder {
|
||||
* @param components
|
||||
* @return
|
||||
*/
|
||||
public Collection<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
|
||||
public static Collection<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
|
||||
UriComponents components) {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
List<Affordance> affordances = new ArrayList<Affordance>();
|
||||
List<Affordance> affordances = new ArrayList<>();
|
||||
|
||||
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), method)) {
|
||||
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), invocation.getMethod())) {
|
||||
|
||||
SpringMvcAffordance affordance = new SpringMvcAffordance(requestMethod, invocation.getMethod());
|
||||
String methodName = invocation.getMethod().getName();
|
||||
|
||||
for (AffordanceModelFactory factory : factories) {
|
||||
affordance.addAffordanceModel(factory.getAffordanceModel(affordance, invocation, components));
|
||||
}
|
||||
Link affordanceLink = new Link(components.toUriString()).withRel(methodName);
|
||||
|
||||
MethodParameters invocationMethodParameters = new MethodParameters(invocation.getMethod());
|
||||
|
||||
ResolvableType inputType = invocationMethodParameters.getParametersWith(RequestBody.class).stream()
|
||||
.findFirst()
|
||||
.map(ResolvableType::forMethodParameter)
|
||||
.orElse(ResolvableType.NONE);
|
||||
|
||||
List<QueryParameter> queryMethodParameters = invocationMethodParameters.getParametersWith(RequestParam.class).stream()
|
||||
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class))
|
||||
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.value(), requestParam.required()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ResolvableType outputType = ResolvableType.forMethodReturnType(invocation.getMethod());
|
||||
|
||||
affordances.add(new Affordance(methodName, affordanceLink, requestMethod, inputType, queryMethodParameters, outputType));
|
||||
|
||||
affordances.add(affordance);
|
||||
}
|
||||
|
||||
return affordances;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.hateoas.mvc;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@@ -45,6 +46,20 @@ public class TypeConstrainedMappingJackson2HttpMessageConverter extends MappingJ
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor to supply all parameters at once.
|
||||
*
|
||||
* @param type
|
||||
* @param supportedMediaTypes
|
||||
* @param objectMapper
|
||||
*/
|
||||
public TypeConstrainedMappingJackson2HttpMessageConverter(Class<?> type, List<MediaType> supportedMediaTypes, ObjectMapper objectMapper) {
|
||||
|
||||
this(type);
|
||||
setSupportedMediaTypes(supportedMediaTypes);
|
||||
setObjectMapper(objectMapper);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#canRead(java.lang.Class, org.springframework.http.MediaType)
|
||||
|
||||
@@ -22,31 +22,34 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class PropertyUtils {
|
||||
|
||||
private final static HashSet<String> FIELDS_TO_IGNORE = new HashSet<String>() {{
|
||||
add("class");
|
||||
add("links");
|
||||
}};
|
||||
private final static HashSet<String> FIELDS_TO_IGNORE = new HashSet<>();
|
||||
|
||||
static {
|
||||
FIELDS_TO_IGNORE.add("class");
|
||||
FIELDS_TO_IGNORE.add("links");
|
||||
};
|
||||
|
||||
public static Map<String, Object> findProperties(Object object) {
|
||||
|
||||
@@ -54,36 +57,30 @@ public class PropertyUtils {
|
||||
return findProperties(((Resource<?>) object).getContent());
|
||||
}
|
||||
|
||||
return Arrays.asList(BeanUtils.getPropertyDescriptors(object.getClass())).stream()
|
||||
.filter(descriptor -> !FIELDS_TO_IGNORE.contains(descriptor.getName()))
|
||||
.filter(descriptor -> hasJsonIgnoreOnTheField(object.getClass(), descriptor))
|
||||
.filter(PropertyUtils::hasJsonIgnoreOnTheReader)
|
||||
.collect(Collectors.toMap(
|
||||
FeatureDescriptor::getName,
|
||||
descriptor -> {
|
||||
return getPropertyDescriptors(object.getClass())
|
||||
.collect(HashMap::new,
|
||||
(hashMap, descriptor) -> {
|
||||
try {
|
||||
return descriptor.getReadMethod().invoke(object);
|
||||
hashMap.put(descriptor.getName(), descriptor.getReadMethod().invoke(object));
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}));
|
||||
},
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
public static List<String> findProperties(ResolvableType resolvableType) {
|
||||
|
||||
public static List<String> findPropertyNames(ResolvableType resolvableType) {
|
||||
|
||||
if (resolvableType.getRawClass().equals(Resource.class)) {
|
||||
return findProperties(resolvableType.resolveGeneric(0));
|
||||
return findPropertyNames(resolvableType.resolveGeneric(0));
|
||||
} else {
|
||||
return findProperties(resolvableType.getRawClass());
|
||||
return findPropertyNames(resolvableType.getRawClass());
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> findProperties(Class<?> clazz) {
|
||||
public static List<String> findPropertyNames(Class<?> clazz) {
|
||||
|
||||
return Arrays.asList(BeanUtils.getPropertyDescriptors(clazz)).stream()
|
||||
.filter(descriptor -> !FIELDS_TO_IGNORE.contains(descriptor.getName()))
|
||||
.filter(descriptor -> hasJsonIgnoreOnTheField(clazz, descriptor))
|
||||
.filter(PropertyUtils::hasJsonIgnoreOnTheReader)
|
||||
return getPropertyDescriptors(clazz)
|
||||
.map(FeatureDescriptor::getName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
@@ -92,13 +89,13 @@ public class PropertyUtils {
|
||||
|
||||
Object obj = BeanUtils.instantiateClass(clazz);
|
||||
|
||||
properties.entrySet().stream().forEach(entry -> {
|
||||
Optional<PropertyDescriptor> possibleProperty = Optional.ofNullable(BeanUtils.getPropertyDescriptor(clazz, entry.getKey()));
|
||||
properties.forEach((key, value) -> {
|
||||
Optional<PropertyDescriptor> possibleProperty = Optional.ofNullable(BeanUtils.getPropertyDescriptor(clazz, key));
|
||||
possibleProperty.ifPresent(property -> {
|
||||
try {
|
||||
Method writeMethod = property.getWriteMethod();
|
||||
ReflectionUtils.makeAccessible(writeMethod);
|
||||
writeMethod.invoke(obj, entry.getValue());
|
||||
writeMethod.invoke(obj, value);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -108,29 +105,43 @@ public class PropertyUtils {
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a {@link Class} and find all properties that are NOT to be ignored, and return them as a {@link Stream}.
|
||||
*
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
private static Stream<PropertyDescriptor> getPropertyDescriptors(Class<?> clazz) {
|
||||
|
||||
return Arrays.stream(BeanUtils.getPropertyDescriptors(clazz))
|
||||
.filter(descriptor -> !FIELDS_TO_IGNORE.contains(descriptor.getName()))
|
||||
.filter(descriptor -> !descriptorToBeIgnoredByJackson(clazz, descriptor))
|
||||
.filter(descriptor -> !toBeIgnoredByJackson(clazz, descriptor.getName()))
|
||||
.filter(descriptor -> !readerIsNotToBeIgnoredByJackson(descriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@link PropertyDescriptor} has {@link JsonIgnore} applied to the field declaration.
|
||||
*
|
||||
* @param object
|
||||
* @param clazz
|
||||
* @param descriptor
|
||||
* @return
|
||||
*/
|
||||
private static boolean hasJsonIgnoreOnTheField(Class<?> clazz, PropertyDescriptor descriptor) {
|
||||
private static boolean descriptorToBeIgnoredByJackson(Class<?> clazz, PropertyDescriptor descriptor) {
|
||||
|
||||
Field descriptorField = ReflectionUtils.findField(clazz, descriptor.getName());
|
||||
|
||||
return isToBeIgnored(AnnotationUtils.getAnnotations(descriptorField));
|
||||
return toBeIgnoredByJackson(AnnotationUtils.getAnnotations(descriptorField));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@link PropertyDescriptor} has {@link JsonIgnore} on the getter.
|
||||
*
|
||||
* @param object
|
||||
* @param descriptor
|
||||
* @return
|
||||
*/
|
||||
private static boolean hasJsonIgnoreOnTheReader(PropertyDescriptor descriptor) {
|
||||
return isToBeIgnored(AnnotationUtils.getAnnotations(descriptor.getReadMethod()));
|
||||
private static boolean readerIsNotToBeIgnoredByJackson(PropertyDescriptor descriptor) {
|
||||
return toBeIgnoredByJackson(AnnotationUtils.getAnnotations(descriptor.getReadMethod()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,17 +150,40 @@ public class PropertyUtils {
|
||||
* @param annotations
|
||||
* @return
|
||||
*/
|
||||
private static boolean isToBeIgnored(Annotation[] annotations) {
|
||||
private static boolean toBeIgnoredByJackson(Annotation[] annotations) {
|
||||
|
||||
if (annotations != null) {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().equals(JsonIgnore.class)) {
|
||||
return !(Boolean) AnnotationUtils.getAnnotationAttributes(annotation).get("value");
|
||||
return (Boolean) AnnotationUtils.getAnnotationAttributes(annotation).get("value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field name is to be ignored due to {@link JsonIgnoreProperties}.
|
||||
*
|
||||
* @param clazz
|
||||
* @param field
|
||||
* @return
|
||||
*/
|
||||
private static boolean toBeIgnoredByJackson(Class<?> clazz, String field) {
|
||||
|
||||
for (Annotation annotation : AnnotationUtils.getAnnotations(clazz)) {
|
||||
if (annotation.annotationType().equals(JsonIgnoreProperties.class)) {
|
||||
String[] namesOfPropertiesToIgnore = (String[]) AnnotationUtils.getAnnotationAttributes(annotation).get("value");
|
||||
for (String propertyToIgnore : namesOfPropertiesToIgnore) {
|
||||
if (propertyToIgnore.equalsIgnoreCase(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user