#362 - Moved ResourceProcessor invoking infrastructure from Spring Data REST.

This commit is contained in:
Oliver Gierke
2016-06-06 11:25:19 +02:00
parent 28687af228
commit 40898cb1ea
5 changed files with 1143 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
/*
* 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
*
* 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.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Field;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* {@link HandlerMethodReturnValueHandler} to post-process the objects returned from controller methods using the
* configured {@link ResourceProcessor}s.
*
* @author Oliver Gierke
* @since 0.20
* @soundtrack Doppelkopf - Balance (Von Abseits)
*/
@RequiredArgsConstructor
public class ResourceProcessorHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
static final ResolvableType RESOURCE_TYPE = ResolvableType.forClass(Resource.class);
static final ResolvableType RESOURCES_TYPE = ResolvableType.forClass(Resources.class);
private static final ResolvableType HTTP_ENTITY_TYPE = ResolvableType.forClass(HttpEntity.class);
static final Field CONTENT_FIELD = ReflectionUtils.findField(Resources.class, "content");
static {
ReflectionUtils.makeAccessible(CONTENT_FIELD);
}
private final @NonNull HandlerMethodReturnValueHandler delegate;
private final @NonNull ResourceProcessorInvoker invoker;
private boolean rootLinksAsHeaders = false;
/**
* @param rootLinksAsHeaders the rootLinksAsHeaders to set
*/
public void setRootLinksAsHeaders(boolean rootLinksAsHeaders) {
this.rootLinksAsHeaders = rootLinksAsHeaders;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodReturnValueHandler#supportsReturnType(org.springframework.core.MethodParameter)
*/
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return delegate.supportsReturnType(returnType);
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodReturnValueHandler#handleReturnValue(java.lang.Object, org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest)
*/
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
Object value = returnValue;
if (returnValue instanceof HttpEntity) {
value = ((HttpEntity<?>) returnValue).getBody();
}
// No post-processable type found - proceed with delegate
if (!ResourceSupport.class.isInstance(value)) {
delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
return;
}
// We have a Resource or Resources - find suitable processors
ResolvableType targetType = ResolvableType.forMethodReturnType(returnType.getMethod());
// Unbox HttpEntity
if (HTTP_ENTITY_TYPE.isAssignableFrom(targetType)) {
targetType = targetType.getGeneric(0);
}
ResolvableType returnValueType = ResolvableType.forClass(value.getClass());
// Returned value is actually of a more specific type, use this type information
if (!getRawType(targetType).equals(getRawType(returnValueType))) {
targetType = returnValueType;
}
ResourceSupport result = invoker.invokeProcessorsFor((ResourceSupport) value, targetType);
delegate.handleReturnValue(rewrapResult(result, returnValue), returnType, mavContainer, webRequest);
}
/**
* Re-wraps the result of the post-processing work into an {@link HttpEntity} or {@link ResponseEntity} if the
* original value was one of those two types. Copies headers and status code from the original value but uses the new
* body.
*
* @param newBody the post-processed value.
* @param originalValue the original input value.
* @return
*/
Object rewrapResult(ResourceSupport newBody, Object originalValue) {
if (!(originalValue instanceof HttpEntity)) {
return newBody;
}
HttpEntity<ResourceSupport> entity = null;
if (originalValue instanceof ResponseEntity) {
ResponseEntity<?> source = (ResponseEntity<?>) originalValue;
entity = new ResponseEntity<ResourceSupport>(newBody, source.getHeaders(), source.getStatusCode());
} else {
HttpEntity<?> source = (HttpEntity<?>) originalValue;
entity = new HttpEntity<ResourceSupport>(newBody, source.getHeaders());
}
return addLinksToHeaderWrapper(entity);
}
private HttpEntity<?> addLinksToHeaderWrapper(HttpEntity<ResourceSupport> entity) {
return rootLinksAsHeaders ? HeaderLinksResponseEntity.wrap(entity) : entity;
}
private static Class<?> getRawType(ResolvableType type) {
Class<?> rawType = type.getRawClass();
return rawType == null ? Object.class : rawType;
}
}

View File

@@ -0,0 +1,411 @@
/*
* Copyright 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
*
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Component to easily invoke all {@link ResourceProcessor} instances registered for values of type
* {@link ResourceSupport}.
*
* @author Oliver Gierke
* @since 0.20
* @soundtrack Doppelkopf - Die fabelhaften Vier (Von Abseits)
*/
public class ResourceProcessorInvoker {
private final List<ProcessorWrapper> processors;
/**
* Creates a new {@link ResourceProcessorInvoker} to consider the given {@link ResourceProcessor} to post-process the
* controller methods return value to before invoking the delegate.
*
* @param processors the {@link ResourceProcessor}s to be considered, must not be {@literal null}.
*/
public ResourceProcessorInvoker(Collection<ResourceProcessor<?>> processors) {
Assert.notNull(processors, "ResourceProcessors must not be null!");
this.processors = new ArrayList<ProcessorWrapper>();
for (ResourceProcessor<?> processor : processors) {
ResolvableType processorType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass());
Class<?> rawType = processorType.getGeneric(0).resolve();
if (Resource.class.isAssignableFrom(rawType)) {
this.processors.add(new ResourceProcessorWrapper(processor));
} else if (Resources.class.isAssignableFrom(rawType)) {
this.processors.add(new ResourcesProcessorWrapper(processor));
} else {
this.processors.add(new DefaultProcessorWrapper(processor));
}
}
Collections.sort(this.processors, AnnotationAwareOrderComparator.INSTANCE);
}
/**
* Invokes all {@link ResourceProcessor} instances registered for the type of the given value.
*
* @param value must not be {@literal null}.
* @return
*/
public <T extends ResourceSupport> T invokeProcessorsFor(T value) {
Assert.notNull(value, "Value must not be null!");
return invokeProcessorsFor(value, ResolvableType.forClass(value.getClass()));
}
/**
* Invokes all {@link ResourceProcessor} instances registered for the type of the given value and reference type.
*
* @param value must not be {@literal null}.
* @param referenceType must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public <T extends ResourceSupport> T invokeProcessorsFor(T value, ResolvableType referenceType) {
Assert.notNull(value, "Value must not be null!");
Assert.notNull(referenceType, "Reference type must not be null!");
// For Resources implementations, process elements first
if (ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(referenceType)) {
Resources<?> resources = (Resources<?>) value;
ResolvableType elementTargetType = ResolvableType.forClass(Resources.class, referenceType.getRawClass())
.getGeneric(0);
List<Object> result = new ArrayList<Object>(resources.getContent().size());
for (Object element : resources) {
ResolvableType elementType = ResolvableType.forClass(element.getClass());
if (!getRawType(elementTargetType).equals(elementType.getRawClass())) {
elementTargetType = elementType;
}
result.add(invokeProcessorsFor(element, elementTargetType));
}
ReflectionUtils.setField(ResourceProcessorHandlerMethodReturnValueHandler.CONTENT_FIELD, resources, result);
}
return (T) invokeProcessorsFor((Object) value, referenceType);
}
/**
* Invokes all registered {@link ResourceProcessor}s registered for the given {@link ResolvableType}.
*
* @param value the object to process
* @param type
* @return
*/
private Object invokeProcessorsFor(Object value, ResolvableType type) {
Object currentValue = value;
// Process actual value
for (ResourceProcessorInvoker.ProcessorWrapper wrapper : this.processors) {
if (wrapper.supports(type, currentValue)) {
currentValue = wrapper.invokeProcessor(currentValue);
}
}
return currentValue;
}
private static boolean isRawTypeAssignable(ResolvableType left, Class<?> right) {
return getRawType(left).isAssignableFrom(right);
}
private static Class<?> getRawType(ResolvableType type) {
Class<?> rawType = type.getRawClass();
return rawType == null ? Object.class : rawType;
}
/**
* Interface to unify interaction with {@link ResourceProcessor}s. The {@link Ordered} rank should be determined by
* the underlying processor.
*
* @author Oliver Gierke
*/
private interface ProcessorWrapper extends Ordered {
/**
* Returns whether the underlying processor supports the given {@link ResolvableType}. It might also additionally
* inspect the object that would eventually be handed to the processor.
*
* @param type the type of object to be post processed, will never be {@literal null}.
* @param value the object that would be passed into the processor eventually, can be {@literal null}.
* @return
*/
boolean supports(ResolvableType type, Object value);
/**
* Performs the actual invocation of the processor. Implementations can be sure
* {@link #supports(ResolvableType, Object)} has been called before and returned {@literal true}.
*
* @param object
*/
Object invokeProcessor(Object object);
}
/**
* Default implementation of {@link ProcessorWrapper} to generically deal with {@link ResourceSupport} types.
*
* @author Oliver Gierke
*/
private static class DefaultProcessorWrapper implements ResourceProcessorInvoker.ProcessorWrapper {
private final ResourceProcessor<?> processor;
private final ResolvableType targetType;
/**
* Creates a new {@link DefaultProcessorWrapper} with the given {@link ResourceProcessor}.
*
* @param processor must not be {@literal null}.
*/
public DefaultProcessorWrapper(ResourceProcessor<?> processor) {
Assert.notNull(processor);
this.processor = processor;
this.targetType = ResolvableType.forClass(ResourceProcessor.class, processor.getClass()).getGeneric(0);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
*/
@Override
public boolean supports(ResolvableType type, Object value) {
return isRawTypeAssignable(targetType, getRawType(type));
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.PostProcessorWrapper#invokeProcessor(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public Object invokeProcessor(Object object) {
return ((ResourceProcessor<ResourceSupport>) processor).process((ResourceSupport) object);
}
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return CustomOrderAwareComparator.INSTANCE.getOrder(processor);
}
/**
* Returns the target type the underlying {@link ResourceProcessor} wants to get invoked for.
*
* @return the targetType
*/
public ResolvableType getTargetType() {
return targetType;
}
}
/**
* {@link ProcessorWrapper} to deal with {@link ResourceProcessor}s for {@link Resource}s. Will fall back to peeking
* into the {@link Resource}'s content for type resolution.
*
* @author Oliver Gierke
*/
private static class ResourceProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper {
/**
* Creates a new {@link ResourceProcessorWrapper} for the given {@link ResourceProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourceProcessorWrapper(ResourceProcessor<?> processor) {
super(processor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
*/
@Override
public boolean supports(ResolvableType type, Object value) {
if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCE_TYPE.isAssignableFrom(type)) {
return false;
}
return super.supports(type, value) && isValueTypeMatch((Resource<?>) value, getTargetType());
}
/**
* Returns whether the given {@link Resource} matches the given target {@link ResolvableType}. We inspect the
* {@link Resource}'s value to determine the match.
*
* @param resource
* @param target must not be {@literal null}.
* @return whether the given {@link Resource} can be assigned to the given target {@link ResolvableType}
*/
private static boolean isValueTypeMatch(Resource<?> resource, ResolvableType target) {
if (resource == null || !isRawTypeAssignable(target, resource.getClass())) {
return false;
}
Object content = resource.getContent();
if (content == null) {
return false;
}
ResolvableType type = findGenericType(target, Resource.class);
return type != null && type.getGeneric(0).isAssignableFrom(ResolvableType.forClass(content.getClass()));
}
private static ResolvableType findGenericType(ResolvableType source, Class<?> type) {
Class<?> rawType = getRawType(source);
if (Object.class.equals(rawType)) {
return null;
}
if (rawType.equals(type)) {
return source;
}
return findGenericType(source.getSuperType(), type);
}
}
/**
* {@link ProcessorWrapper} for {@link ResourceProcessor}s targeting {@link Resources}. Will peek into the content of
* the {@link Resources} for type matching decisions if needed.
*
* @author Oliver Gierke
*/
public static class ResourcesProcessorWrapper extends ResourceProcessorInvoker.DefaultProcessorWrapper {
/**
* Creates a new {@link ResourcesProcessorWrapper} for the given {@link ResourceProcessor}.
*
* @param processor must not be {@literal null}.
*/
public ResourcesProcessorWrapper(ResourceProcessor<?> processor) {
super(processor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.DefaultProcessorWrapper#supports(org.springframework.core.ResolvableType, java.lang.Object)
*/
@Override
public boolean supports(ResolvableType type, Object value) {
if (!ResourceProcessorHandlerMethodReturnValueHandler.RESOURCES_TYPE.isAssignableFrom(type)) {
return false;
}
return super.supports(type, value) && isValueTypeMatch((Resources<?>) value, getTargetType());
}
/**
* Returns whether the given {@link Resources} instance matches the given {@link ResolvableType}. We predict this by
* inspecting the first element of the content of the {@link Resources}.
*
* @param resources the {@link Resources} to inspect.
* @param target that target {@link ResolvableType}.
* @return
*/
static boolean isValueTypeMatch(Resources<?> resources, ResolvableType target) {
if (resources == null) {
return false;
}
Collection<?> content = resources.getContent();
if (content.isEmpty()) {
return false;
}
ResolvableType superType = null;
for (Class<?> resourcesType : Arrays.<Class<?>> asList(resources.getClass(), Resources.class)) {
superType = ResolvableType.forClass(resourcesType, getRawType(target));
if (superType != null) {
break;
}
}
if (superType == null) {
return false;
}
Object element = content.iterator().next();
ResolvableType resourceType = superType.getGeneric(0);
if (element instanceof Resource) {
return ResourceProcessorWrapper.isValueTypeMatch((Resource<?>) element, resourceType);
} else if (element instanceof EmbeddedWrapper) {
return isRawTypeAssignable(resourceType, ((EmbeddedWrapper) element).getRelTargetType());
}
return false;
}
}
/**
* Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder(Object)} public to allow it
* being used in a standalone fashion.
*
* @author Oliver Gierke
*/
private static class CustomOrderAwareComparator extends AnnotationAwareOrderComparator {
public static ResourceProcessorInvoker.CustomOrderAwareComparator INSTANCE = new CustomOrderAwareComparator();
@Override
protected int getOrder(Object obj) {
return super.getOrder(obj);
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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
*
* 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.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.util.ReflectionUtils;
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 ResourceProcessorHandlerMethodReturnValueHandler} which will invoke the {@link ResourceProcessor}
* 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 ResourceProcessorInvokingHandlerAdapter extends RequestMappingHandlerAdapter {
private static final Method RETURN_VALUE_HANDLER_METHOD = ReflectionUtils
.findMethod(ResourceProcessorInvokingHandlerAdapter.class, "getReturnValueHandlers");
private @NonNull final ResourceProcessorInvoker 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<HandlerMethodReturnValueHandler>();
newHandlers.add(new ResourceProcessorHandlerMethodReturnValueHandler(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 = ReflectionUtils.invokeMethod(RETURN_VALUE_HANDLER_METHOD, this);
if (handlers instanceof HandlerMethodReturnValueHandlerComposite) {
return (HandlerMethodReturnValueHandlerComposite) handlers;
}
return new HandlerMethodReturnValueHandlerComposite()
.addHandlers((List<? extends HandlerMethodReturnValueHandler>) handlers);
}
}