#859 - Fix RepresentationModelProcessor to smoothly register with Spring MVC.

Some more polishing on internal of Processor and Invoker machinery regarding resource -> model, etc.
This commit is contained in:
Greg Turnquist
2019-03-07 10:08:00 -06:00
committed by Oliver Drotbohm
parent 0439c5f708
commit 1e621784e7
16 changed files with 583 additions and 141 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.hateoas.config;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -27,13 +28,18 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.RepresentationModelProcessor;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorHandlerMethodReturnValueHandler;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.hateoas.server.mvc.UriComponentsContributor;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.NonNull;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -52,6 +58,13 @@ class WebMvcHateoasConfiguration {
return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
}
@Bean
HypermediaRepresentationModelBeanProcessorPostProcessor hypermediaRepresentionModelProcessorConfigurator(
List<RepresentationModelProcessor<?>> processors) {
return new HypermediaRepresentationModelBeanProcessorPostProcessor(processors);
}
@Bean
static HypermediaRestTemplateBeanPostProcessor restTemplateBeanPostProcessor(
ObjectProvider<HypermediaWebMvcConfigurer> configurer) {
@@ -92,6 +105,34 @@ class WebMvcHateoasConfiguration {
}
}
/**
* @author Greg Turnquist
*/
@RequiredArgsConstructor
static class HypermediaRepresentationModelBeanProcessorPostProcessor implements BeanPostProcessor {
private final List<RepresentationModelProcessor<?>> processors;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (RequestMappingHandlerAdapter.class.isInstance(bean)) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
HandlerMethodReturnValueHandlerComposite delegate = new HandlerMethodReturnValueHandlerComposite();
delegate.addHandlers(adapter.getReturnValueHandlers());
RepresentationModelProcessorHandlerMethodReturnValueHandler handler = new RepresentationModelProcessorHandlerMethodReturnValueHandler(
delegate, new RepresentationModelProcessorInvoker(processors));
adapter.setReturnValueHandlers(Collections.singletonList(handler));
}
return bean;
}
}
/**
* {@link BeanPostProcessor} to register hypermedia support with {@link RestTemplate} instances found in the
* application context.

View File

@@ -32,8 +32,8 @@ public interface RepresentationModelProcessor<T extends RepresentationModel<?>>
/**
* Processes the given representation model, add links, alter the domain data etc.
*
* @param resource
* @return the processed resource
* @param model
* @return the processed model
*/
T process(T resource);
T process(T model);
}

View File

@@ -47,8 +47,8 @@ import org.springframework.web.method.support.ModelAndViewContainer;
@RequiredArgsConstructor
public class RepresentationModelProcessorHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
static final ResolvableType RESOURCE_TYPE = ResolvableType.forRawClass(EntityModel.class);
static final ResolvableType RESOURCES_TYPE = ResolvableType.forRawClass(CollectionModel.class);
static final ResolvableType ENTITY_MODEL_TYPE = ResolvableType.forRawClass(EntityModel.class);
static final ResolvableType COLLECTION_MODEL_TYPE = ResolvableType.forRawClass(CollectionModel.class);
private static final ResolvableType HTTP_ENTITY_TYPE = ResolvableType.forRawClass(HttpEntity.class);
static final Field CONTENT_FIELD = ReflectionUtils.findField(CollectionModel.class, "content");

View File

@@ -37,6 +37,7 @@ import org.springframework.util.ReflectionUtils;
* {@link RepresentationModel}.
*
* @author Oliver Gierke
* @author Greg Turnquist
* @since 0.20
* @soundtrack Doppelkopf - Die fabelhaften Vier (Von Abseits)
*/
@@ -64,9 +65,9 @@ public class RepresentationModelProcessorInvoker {
if (rawType != null) {
if (EntityModel.class.isAssignableFrom(rawType)) {
this.processors.add(new ResourceProcessorWrapper(processor));
this.processors.add(new EntityModelProcessorWrapper(processor));
} else if (CollectionModel.class.isAssignableFrom(rawType)) {
this.processors.add(new ResourcesProcessorWrapper(processor));
this.processors.add(new CollectionModelProcessorWrapper(processor));
} else {
this.processors.add(new DefaultProcessorWrapper(processor));
}
@@ -104,9 +105,10 @@ public class RepresentationModelProcessorInvoker {
Assert.notNull(referenceType, "Reference type must not be null!");
// For Resources implementations, process elements first
if (RepresentationModelProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(referenceType)) {
if (RepresentationModelProcessorHandlerMethodReturnValueHandler.COLLECTION_MODEL_TYPE
.isAssignableFrom(referenceType)) {
CollectionModel<?> resources = (CollectionModel<?>) value;
CollectionModel<?> collectionModel = (CollectionModel<?>) value;
Class<?> rawClass = referenceType.getRawClass();
if (rawClass == null) {
@@ -114,9 +116,9 @@ public class RepresentationModelProcessorInvoker {
}
ResolvableType elementTargetType = ResolvableType.forClass(CollectionModel.class, rawClass).getGeneric(0);
List<Object> result = new ArrayList<>(resources.getContent().size());
List<Object> result = new ArrayList<>(collectionModel.getContent().size());
for (Object element : resources) {
for (Object element : collectionModel) {
ResolvableType elementType = ResolvableType.forClass(element.getClass());
@@ -128,8 +130,8 @@ public class RepresentationModelProcessorInvoker {
}
if (RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD != null) {
ReflectionUtils.setField(RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources,
result);
ReflectionUtils.setField(RepresentationModelProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD,
collectionModel, result);
}
}
@@ -269,14 +271,14 @@ public class RepresentationModelProcessorInvoker {
*
* @author Oliver Gierke
*/
private static class ResourceProcessorWrapper extends RepresentationModelProcessorInvoker.DefaultProcessorWrapper {
private static class EntityModelProcessorWrapper extends RepresentationModelProcessorInvoker.DefaultProcessorWrapper {
/**
* Creates a new {@link ResourceProcessorWrapper} for the given {@link RepresentationModelProcessor}.
* Creates a new {@link EntityModelProcessorWrapper} for the given {@link RepresentationModelProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourceProcessorWrapper(RepresentationModelProcessor<?> processor) {
public EntityModelProcessorWrapper(RepresentationModelProcessor<?> processor) {
super(processor);
}
@@ -287,7 +289,7 @@ public class RepresentationModelProcessorInvoker {
@Override
public boolean supports(ResolvableType type, Object value) {
if (!RepresentationModelProcessorHandlerMethodReturnValueHandler.RESOURCE_TYPE.isAssignableFrom(type)) {
if (!RepresentationModelProcessorHandlerMethodReturnValueHandler.ENTITY_MODEL_TYPE.isAssignableFrom(type)) {
return false;
}
@@ -298,24 +300,26 @@ public class RepresentationModelProcessorInvoker {
* Returns whether the given {@link EntityModel} matches the given target {@link ResolvableType}. We inspect the
* {@link EntityModel}'s value to determine the match.
*
* @param resource can be {@literal null}.
* @param entityModel can be {@literal null}.
* @param target can be {@literal null}.
* @return whether the given {@link EntityModel} can be assigned to the given target {@link ResolvableType}
*/
private static boolean isValueTypeMatch(@Nullable EntityModel<?> resource, @Nullable ResolvableType target) {
private static boolean isValueTypeMatch(@Nullable EntityModel<?> entityModel, @Nullable ResolvableType target) {
if (resource == null || !isRawTypeAssignable(target, resource.getClass())) {
if (entityModel == null || !isRawTypeAssignable(target, entityModel.getClass())) {
return false;
}
Object content = resource.getContent();
Object content = entityModel.getContent();
if (content == null) {
return false;
}
ResolvableType type = findGenericType(target, EntityModel.class);
return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass()));
return target.isAssignableFrom(content.getClass()) || //
(type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass())));
}
@Nullable
@@ -345,14 +349,15 @@ public class RepresentationModelProcessorInvoker {
*
* @author Oliver Gierke
*/
public static class ResourcesProcessorWrapper extends RepresentationModelProcessorInvoker.DefaultProcessorWrapper {
public static class CollectionModelProcessorWrapper
extends RepresentationModelProcessorInvoker.DefaultProcessorWrapper {
/**
* Creates a new {@link ResourcesProcessorWrapper} for the given {@link RepresentationModelProcessor}.
* Creates a new {@link CollectionModelProcessorWrapper} for the given {@link RepresentationModelProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourcesProcessorWrapper(RepresentationModelProcessor<?> processor) {
public CollectionModelProcessorWrapper(RepresentationModelProcessor<?> processor) {
super(processor);
}
@@ -363,28 +368,31 @@ public class RepresentationModelProcessorInvoker {
@Override
public boolean supports(ResolvableType type, Object value) {
if (!RepresentationModelProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(type)) {
if (!RepresentationModelProcessorHandlerMethodReturnValueHandler.COLLECTION_MODEL_TYPE.isAssignableFrom(type)) {
return false;
}
return super.supports(type, value) && isValueTypeMatch((CollectionModel<?>) value, getTargetType());
boolean defaultSupports = super.supports(type, value);
boolean valueTypeMatch = isValueTypeMatch((CollectionModel<?>) value, getTargetType());
return defaultSupports && valueTypeMatch;
}
/**
* Returns whether the given {@link CollectionModel} instance matches the given {@link ResolvableType}. We predict
* this by inspecting the first element of the content of the {@link CollectionModel}.
*
* @param resources the {@link CollectionModel} to inspect.
* @param collectionModel the {@link CollectionModel} to inspect.
* @param target that target {@link ResolvableType}.
* @return
*/
static boolean isValueTypeMatch(@Nullable CollectionModel<?> resources, ResolvableType target) {
if (resources == null) {
static boolean isValueTypeMatch(@Nullable CollectionModel<?> collectionModel, ResolvableType target) {
if (collectionModel == null) {
return false;
}
Collection<?> content = resources.getContent();
Collection<?> content = collectionModel.getContent();
if (content.isEmpty()) {
return false;
@@ -392,9 +400,9 @@ public class RepresentationModelProcessorInvoker {
ResolvableType superType = null;
for (Class<?> resourcesType : Arrays.<Class<?>> asList(resources.getClass(), CollectionModel.class)) {
for (Class<?> collectionModelType : Arrays.<Class<?>> asList(collectionModel.getClass(), CollectionModel.class)) {
superType = getSuperType(target, resourcesType);
superType = getSuperType(target, collectionModelType);
if (superType != null) {
break;
@@ -409,7 +417,7 @@ public class RepresentationModelProcessorInvoker {
ResolvableType resourceType = superType.getGeneric(0);
if (element instanceof EntityModel) {
return ResourceProcessorWrapper.isValueTypeMatch((EntityModel<?>) element, resourceType);
return EntityModelProcessorWrapper.isValueTypeMatch((EntityModel<?>) element, resourceType);
} else if (element instanceof EmbeddedWrapper) {
return isRawTypeAssignable(resourceType, ((EmbeddedWrapper) element).getRelTargetType());
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.server.mvc;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import org.springframework.hateoas.server.RepresentationModelProcessor;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
* Special {@link RequestMappingHandlerAdapter} that tweaks the {@link HandlerMethodReturnValueHandlerComposite} to be
* proxied by a {@link RepresentationModelProcessorHandlerMethodReturnValueHandler} which will invoke the
* {@link RepresentationModelProcessor} s found in the application context and eventually delegate to the originally
* configured {@link HandlerMethodReturnValueHandler}.
* <p/>
* This is a separate component as it might make sense to deploy it in a standalone SpringMVC application to enable post
* processing. It would actually make most sense in Spring HATEOAS project.
*
* @author Oliver Gierke
* @author Phil Webb
* @since 0.20
* @soundtrack Dopplekopf - Regen für immer (Von Abseits)
*/
@RequiredArgsConstructor
public class RepresentationModelProcessorInvokingHandlerAdapter extends RequestMappingHandlerAdapter {
private @NonNull final RepresentationModelProcessorInvoker invoker;
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
// Retrieve actual handlers to use as delegate
HandlerMethodReturnValueHandlerComposite oldHandlers = getReturnValueHandlersComposite();
// Set up ResourceProcessingHandlerMethodResolver to delegate to originally configured ones
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>();
newHandlers.add(new RepresentationModelProcessorHandlerMethodReturnValueHandler(oldHandlers, invoker));
// Configure the new handler to be used
this.setReturnValueHandlers(newHandlers);
}
/**
* Gets a {@link HandlerMethodReturnValueHandlerComposite} for return handlers, dealing with API changes introduced in
* Spring 4.0.
*
* @return a HandlerMethodReturnValueHandlerComposite
*/
@SuppressWarnings("unchecked")
private HandlerMethodReturnValueHandlerComposite getReturnValueHandlersComposite() {
Object handlers = this.getReturnValueHandlers();
if (handlers instanceof HandlerMethodReturnValueHandlerComposite) {
return (HandlerMethodReturnValueHandlerComposite) handlers;
}
return new HandlerMethodReturnValueHandlerComposite() //
.addHandlers((List<? extends HandlerMethodReturnValueHandler>) handlers);
}
}