#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:
committed by
Oliver Drotbohm
parent
0439c5f708
commit
1e621784e7
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user