DATAREST-221 - Added support for projections.

This commit introduces support to access resources via projections, which means naming a dedicated set of properties of the entity to be exposed and being able to refer to that set through a request parameter.

## General usage

Projections are defined as interfaces that mimic the properties of the domain class to be exported:

@Projection(types = Customer.class, name = "summary")
interface Summary {
  String getFirstname();
  String getLastname();
  AddressSummary getAddress();
}

interface AddressSummary() {
  String getZipCode();
}

The projection interface can be annotated with @Projection to be auto-discovered. We scan all packages in which we find domain types to be exported for projection types and auto-register them. For manual registration, use RepositoryRestConfiguration.projectionDefinitionConfiguration().addProjection(…) and manually register them.

If a projection is registered for a given type, this will be indicated via a "projection" template variable in the URI pointing to resources with projections. The name of the variable can also be configured on ProjectionDefinitionConfiguration.

## Internals

The projection interfaces are consider bean property delegates by default. This means, that for the above interfaces we will lookup the firstname, lastname and address property of the projection target. In the case of address we re-project the result of the proxy target invocation with a sub-projection onto AddressSummary.

For more advanced use-cases you can annotate a method of the projection interface with @Value and use a SpEL expression to invoke further functionality and return that to be rendered:

interface MyProjection {

  @Value("#{@myBean.someMethod(target)}")
  SubProjection getValue();
}

This projection would call the someMethod(…) method on a Spring bean named myBean handing the proxy target to the method. The result will be projected in turn onto a type called SubProjection.

As the projection objects are exposed to Jackson as is, they can be annotated with Jackson annotations to further customize the representation.
This commit is contained in:
Oliver Gierke
2014-02-24 08:15:51 +01:00
parent faf9a48f30
commit af7e15b8e6
44 changed files with 1947 additions and 191 deletions

View File

@@ -16,7 +16,7 @@
</parent>
<properties>
<spring.hateoas>0.9.0.RELEASE</spring.hateoas>
<spring.hateoas>0.10.0.BUILD-SNAPSHOT</spring.hateoas>
<springplugin>1.0.0.RELEASE</springplugin>
<evoinflector>1.0.1</evoinflector>
</properties>
@@ -59,6 +59,12 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2014 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.data.rest.core.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to tie a particular projection type to a source type. Used to find projection interfaces at startup time.
*
* @author Oliver Gierke
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
public @interface Projection {
/**
* The type the projection type is bound to.
*
* @return
*/
Class<?>[] types();
/**
* The name of projection to refer to.
*
* @return
*/
String name() default "";
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright 2014 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.data.rest.core.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Wrapper class to register projection definitions for later lookup by name and source type.
*
* @author Oliver Gierke
*/
public class ProjectionDefinitionConfiguration implements ProjectionDefinitions {
private static final String PROJECTION_ANNOTATION_NOT_FOUND = "Projection annotation not found on %s! Either add the annotation or hand source type to the registration manually!";
private static final String DEFAULT_PROJECTION_PARAMETER_NAME = "projection";
private final Map<ProjectionDefinitionKey, Class<?>> projectionDefinitions;
private String parameterName = DEFAULT_PROJECTION_PARAMETER_NAME;
public ProjectionDefinitionConfiguration() {
this.projectionDefinitions = new HashMap<ProjectionDefinitionKey, Class<?>>();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.projection.ProjectionDefinitions#getParameterName()
*/
public String getParameterName() {
return parameterName;
};
/**
* Configures the request parameter name to be used to accept the projection name to be returned.
*
* @param parameterName defaults to {@value ProjectionDefinitionConfiguration#DEFAULT_PROJECTION_PARAMETER_NAME}, will
* be set back to this default if {@literal null} or an empty value is configured.
*/
public void setParameterName(String parameterName) {
this.parameterName = StringUtils.hasText(parameterName) ? parameterName : DEFAULT_PROJECTION_PARAMETER_NAME;
}
/**
* Adds the given projection type to the configuration. The type has to be annotated with {@link Projection} for
* additional metadata.
*
* @param projectionType must not be {@literal null}.
* @return
* @see Projection
*/
public ProjectionDefinitionConfiguration addProjection(Class<?> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null!");
Projection annotation = AnnotationUtils.findAnnotation(projectionType, Projection.class);
if (annotation == null) {
throw new IllegalArgumentException(String.format(PROJECTION_ANNOTATION_NOT_FOUND, projectionType));
}
String name = annotation.name();
Class<?>[] sourceTypes = annotation.types();
return StringUtils.hasText(name) ? addProjection(projectionType, name, sourceTypes) : addProjection(projectionType,
sourceTypes);
}
/**
* Adds a projection type for the given source types. The name of the projection will be defaulted to the
* uncapitalized simply class name.
*
* @param projectionType must not be {@literal null}.
* @param sourceTypes must not be {@literal null} or empty.
* @return
*/
public ProjectionDefinitionConfiguration addProjection(Class<?> projectionType, Class<?>... sourceTypes) {
Assert.notNull(projectionType, "Projection type must not be null!");
return addProjection(projectionType, StringUtils.uncapitalize(projectionType.getSimpleName()), sourceTypes);
}
/**
* Adds the given projection type for the given source types under the given name.
*
* @param projectionType must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @param sourceTypes must not be {@literal null} or empty.
* @return
*/
public ProjectionDefinitionConfiguration addProjection(Class<?> projectionType, String name, Class<?>... sourceTypes) {
Assert.notNull(projectionType, "Projection type must not be null!");
Assert.hasText(name, "Name must not be null or empty!");
Assert.notEmpty(sourceTypes, "Source types must not be null!");
for (Class<?> sourceType : sourceTypes) {
this.projectionDefinitions.put(new ProjectionDefinitionKey(sourceType, name), projectionType);
}
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.config.ProjectionDefinitions#getProjectionType(java.lang.Class, java.lang.String)
*/
@Override
public Class<?> getProjectionType(Class<?> sourceType, String name) {
return projectionDefinitions.get(new ProjectionDefinitionKey(sourceType, name));
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.projection.ProjectionDefinitions#hasProjectionFor(java.lang.Class)
*/
@Override
public boolean hasProjectionFor(Class<?> sourceType) {
for (ProjectionDefinitionKey key : projectionDefinitions.keySet()) {
if (key.sourceType.equals(sourceType)) {
return true;
}
}
return false;
}
/**
* Value object to define lookup keys for projections.
*
* @author Oliver Gierke
*/
static final class ProjectionDefinitionKey {
private final Class<?> sourceType;
private final String name;
/**
* Creates a new {@link ProjectionDefinitionKey} for the given source type and name;
*
* @param sourceType must not be {@literal null}.
* @param name must not be {@literal null} or empty.
*/
public ProjectionDefinitionKey(Class<?> sourceType, String name) {
Assert.notNull(sourceType, "Source type must not be null!");
Assert.hasText(name, "Name must not be null or empty!");
this.sourceType = sourceType;
this.name = name;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ProjectionDefinitionKey)) {
return false;
}
ProjectionDefinitionKey that = (ProjectionDefinitionKey) obj;
return this.name.equals(that.name) && this.sourceType.equals(that.sourceType);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 31;
result += name.hashCode();
result += sourceType.hashCode();
return result;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
@@ -35,7 +35,7 @@ public class RepositoryRestConfiguration {
private int defaultPageSize = 20;
private int maxPageSize = 1000;
private String pageParamName = "page";
private String limitParamName = "limit";
private String limitParamName = "size";
private String sortParamName = "sort";
private MediaType defaultMediaType = MediaTypes.HAL_JSON;
private boolean returnBodyOnCreate = false;
@@ -43,6 +43,25 @@ public class RepositoryRestConfiguration {
private List<Class<?>> exposeIdsFor = new ArrayList<Class<?>>();
private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration();
private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration();
private final ProjectionDefinitionConfiguration projectionConfiguration;
/**
* Creates a new default {@link RepositoryRestConfiguration}.
*/
public RepositoryRestConfiguration() {
this(new ProjectionDefinitionConfiguration());
}
/**
* Creates a new {@link RepositoryRestConfiguration} with the given {@link ProjectionDefinitionConfiguration}.
*
* @param projectionConfiguration must not be {@literal null}.
*/
public RepositoryRestConfiguration(ProjectionDefinitionConfiguration projectionConfiguration) {
Assert.notNull(projectionConfiguration, "ProjectionDefinitionConfiguration must not be null!");
this.projectionConfiguration = projectionConfiguration;
}
/**
* The base URI against which the exporter should calculate its links.
@@ -80,7 +99,7 @@ public class RepositoryRestConfiguration {
* @return {@literal this}
*/
public RepositoryRestConfiguration setDefaultPageSize(int defaultPageSize) {
Assert.isTrue((defaultPageSize > 0), "Page size must be greater than 0.");
Assert.isTrue(defaultPageSize > 0, "Page size must be greater than 0.");
this.defaultPageSize = defaultPageSize;
return this;
}
@@ -101,7 +120,7 @@ public class RepositoryRestConfiguration {
* @return {@literal this}
*/
public RepositoryRestConfiguration setMaxPageSize(int maxPageSize) {
Assert.isTrue((defaultPageSize > 0), "Maximum page size must be greater than 0.");
Assert.isTrue(defaultPageSize > 0, "Maximum page size must be greater than 0.");
this.maxPageSize = maxPageSize;
return this;
}
@@ -328,4 +347,13 @@ public class RepositoryRestConfiguration {
Collections.addAll(exposeIdsFor, domainTypes);
return this;
}
/**
* Returns the {@link ProjectionDefinitionConfiguration} to register addition projections.
*
* @return
*/
public ProjectionDefinitionConfiguration projectionConfiguration() {
return projectionConfiguration;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.util.Assert;
/**
* {@link MethodInterceptor} to delegate the invocation to a different {@link MethodInterceptor} but creating a
* projecting proxy in case the returned value is not of the return type of the invoked method.
*
* @author Oliver Gierke
*/
class ProjectingMethodInterceptor implements MethodInterceptor {
private final ProjectionFactory factory;
private final MethodInterceptor delegate;
/**
* Creates a new {@link ProjectingMethodInterceptor} using the given {@link ProjectionFactory} and delegate
* {@link MethodInterceptor}.
*
* @param factory the {@link ProjectionFactory} to use to create projections if types do not match.
* @param delegate the {@link MethodInterceptor} to trigger to create the source value.
*/
public ProjectingMethodInterceptor(ProjectionFactory factory, MethodInterceptor delegate) {
Assert.notNull(factory, "ProjectionFactory must not be null!");
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!");
this.factory = factory;
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = delegate.invoke(invocation);
if (result == null) {
return null;
}
Class<?> returnType = invocation.getMethod().getReturnType();
return returnType.isAssignableFrom(result.getClass()) ? result : factory.createProjection(result, returnType);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2014 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.data.rest.core.projection;
/**
* Interface to allow the lookup of a projection interface by source type and name. This allows the definition of
* projections with the same name for different source types.
*
* @author Oliver Gierke
*/
public interface ProjectionDefinitions {
/**
* Returns the projection type for the given source type and name.
*
* @param sourceType must not be {@literal null}.
* @param name must not be {@literal null} or empty.
* @return
*/
Class<?> getProjectionType(Class<?> sourceType, String name);
/**
* Returns whether we have a projection registered for the given source type.
*
* @param sourceType must not be {@literal null}.
* @return
*/
boolean hasProjectionFor(Class<?> sourceType);
/**
* Returns the request parameter to be used to expose the projection to the web.
*
* @return the parameterName will never be {@literal null} or empty.
*/
String getParameterName();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2014 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.data.rest.core.projection;
/**
* A factory to create projecting instances for other objects usually used to allow easy creation of representation
* projections to define which properties of a domain objects shall be exported in which way.
*
* @author Oliver Gierke
*/
public interface ProjectionFactory {
/**
* Creates a projection of the given type for the given source object. The individual mapping strategy is defined by
* the implementations.
*
* @param source the object to create a projection for, can be {@literal null}
* @param projectionType the type to create.
* @return
*/
<T> T createProjection(Object source, Class<T> projectionType);
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
import org.springframework.util.Assert;
/**
* Method interceptor to forward a delegation to bean property accessor methods to the property of a given target.
*
* @author Oliver Gierke
*/
class PropertyAccessingMethodInterceptor implements MethodInterceptor {
private final BeanWrapper target;
/**
* Creates a new {@link PropertyAccessingMethodInterceptor} for the given target object.
*
* @param target must not be {@literal null}.
* @param factory must not be {@literal null}.
*/
public PropertyAccessingMethodInterceptor(Object target) {
Assert.notNull(target, "Proxy target must not be null!");
this.target = new DirectFieldAccessFallbackBeanWrapper(target);
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
PropertyDescriptor descriptor = BeanUtils.findPropertyForMethod(method);
if (descriptor == null) {
throw new IllegalStateException("Invoked method is not a property accessor!");
}
return target.getPropertyValue(descriptor.getName());
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.util.AnnotationDetectionMethodCallback;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* A {@link ProjectionFactory} to create JDK proxies to back interfaces and handle method invocations on them. By
* default two different kinds of methods are supported:
* <ol>
* <li>Bean property accessor methods - invocations will be delegated into a property lookup on the target instance.</li>
* <li>Arbitrary methods annotated with {@link Value} to contain a SpEL expression, which will be evaluated on
* invocation. The expressions can use {@code target} to refer to the proxy target.</li>
* </ol>
* In case the dlegating lookups result in an object of different type that the projection interface method's return
* type, another projection will be created to transparently mitigate between the types.
*
* @author Oliver Gierke
*/
public class ProxyProjectionFactory implements ProjectionFactory {
private final Map<Class<?>, Boolean> typeCache = new HashMap<Class<?>, Boolean>();
private BeanFactory beanFactory;
/**
* Creates a new {@link ProxyProjectionFactory} using the given {@link BeanFactory}.
*
* @param beanFactory can be {@literal null}. If {@literal null}, SpEL expressions at projection interfaces cannot use
* bean references.
*/
public ProxyProjectionFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.projection.ProjectionFactory#createProjection(java.lang.Object, java.lang.Class)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T createProjection(Object source, Class<T> projectionType) {
Assert.isTrue(projectionType.isInterface(), "Projection type must be an interface!");
if (source == null) {
return null;
}
ProxyFactory factory = new ProxyFactory();
factory.setTarget(source);
factory.setOpaque(true);
factory.setInterfaces(projectionType, TargetClassAware.class);
factory.addAdvice(new TargetClassAwareMethodInterceptor(source.getClass()));
factory.addAdvice(getMethodInterceptor(source, projectionType));
return (T) factory.getProxy();
}
/**
* Returns the {@link MethodInterceptor} to add to the proxy.
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
* @return
*/
private MethodInterceptor getMethodInterceptor(Object source, Class<?> target) {
MethodInterceptor propertyInvocationInterceptor = new PropertyAccessingMethodInterceptor(source);
return new ProjectingMethodInterceptor(this, getSpelMethodInterceptorIfNecessary(target,
propertyInvocationInterceptor));
}
/**
* Inspects the given target type for methods with {@link Value} annotations and caches the result. Will create a
* {@link SpelEvaluatingMethodInterceptor} if an annotation was found or return the delegate as is if not.
*
* @param target the proxy target type.
* @param delegate the root {@link MethodInterceptor}.
* @return
*/
private MethodInterceptor getSpelMethodInterceptorIfNecessary(Class<?> target, MethodInterceptor delegate) {
if (!typeCache.containsKey(target)) {
AnnotationDetectionMethodCallback<Value> callback = new AnnotationDetectionMethodCallback<Value>(Value.class);
ReflectionUtils.doWithMethods(target, callback);
typeCache.put(target, callback.hasFoundAnnotation());
}
return typeCache.get(target) ? new SpelEvaluatingMethodInterceptor(delegate, target, beanFactory) : delegate;
}
/**
* Extension of {@link org.springframework.aop.TargetClassAware} to be able to ignore the getter on JSON rendering.
*
* @author Oliver Gierke
*/
public static interface TargetClassAware extends org.springframework.aop.TargetClassAware {
@JsonIgnore
Class<?> getTargetClass();
}
/**
* Custom {@link MethodInterceptor} to expose the proxy target class even if we set
* {@link ProxyFactory#setOpaque(boolean)} to true to prevent properties on {@link Advised} to be rendered.
*
* @author Oliver Gierke
*/
private static class TargetClassAwareMethodInterceptor implements MethodInterceptor {
private static final Method GET_TARGET_CLASS_METHOD;
private final Class<?> targetClass;
static {
try {
GET_TARGET_CLASS_METHOD = TargetClassAware.class.getMethod("getTargetClass");
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}
/**
* Creates a new {@link TargetClassAwareMethodInterceptor} with the given target class.
*
* @param targetClass must not be {@literal null}.
*/
public TargetClassAwareMethodInterceptor(Class<?> targetClass) {
Assert.notNull(targetClass, "Target class must not be null!");
this.targetClass = targetClass;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (invocation.getMethod().equals(GET_TARGET_CLASS_METHOD)) {
return targetClass;
}
return invocation.proceed();
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link MethodInterceptor} to invoke a SpEL expression to compute the method result. Will forward the resolution to a
* delegate {@link MethodInterceptor} if no {@link Value} annotation is found.
*
* @author Oliver Gierke
*/
class SpelEvaluatingMethodInterceptor implements MethodInterceptor {
private final SpelExpressionParser parser;
private final ParserContext parserContext;
private final EvaluationContext evaluationContext;
private final MethodInterceptor delegate;
/**
* Creates a new {@link SpelEvaluatingMethodInterceptor} delegating to the given {@link MethodInterceptor} as fallback
* and exposing the given target object via {@code target} to the SpEl expressions. If a {@link BeanFactory} is given,
* bean references in SpEl expressions can be resolved as well.
*
* @param delegate must not be {@literal null}.
* @param target must not be {@literal null}.
* @param beanFactory can be {@literal null}.
*/
public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, BeanFactory beanFactory) {
Assert.notNull(delegate, "Delegate MethodInterceptor must not be null!");
Assert.notNull(target, "TargetObject must not be null!");
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new TargetWrapper(target));
if (beanFactory != null) {
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
this.evaluationContext = evaluationContext;
this.parser = new SpelExpressionParser();
this.parserContext = new TemplateParserContext();
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Value annotation = method.getAnnotation(Value.class);
if (annotation == null || !StringUtils.hasText(annotation.value())) {
return delegate.invoke(invocation);
}
Expression expression = parser.parseExpression(annotation.value(), parserContext);
return expression.getValue(evaluationContext);
}
/**
* Wrapper class to expose an object to the SpEL expression as {@code target}.
*
* @author Oliver Gierke
*/
static class TargetWrapper {
private final Object target;
public TargetWrapper(Object target) {
this.target = target;
}
/**
* @return the target
*/
public Object getTarget() {
return target;
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2014 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.data.rest.core.config;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration.ProjectionDefinitionKey;
/**
* Unit tests for {@link ProjectionDefinitionConfiguration}.
*
* @author Oliver Gierke
*/
@SuppressWarnings("rawtypes")
public class ProjectionDefinitionConfigurationUnitTests {
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullProjectionTypeForAutoConfiguration() {
new ProjectionDefinitionConfiguration().addProjection(null);
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsUnannotatedClassForConfigurationShortcut() {
new ProjectionDefinitionConfiguration().addProjection(String.class);
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullProjectionTypeForManualConfiguration() {
new ProjectionDefinitionConfiguration().addProjection(null, "name", Object.class);
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullNameForManualConfiguration() {
new ProjectionDefinitionConfiguration().addProjection(String.class, (String) null, Object.class);
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsEmptyNameForManualConfiguration() {
new ProjectionDefinitionConfiguration().addProjection(String.class, "", Object.class);
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsEmptySourceTypes() {
new ProjectionDefinitionConfiguration().addProjection(String.class, "name", new Class<?>[0]);
}
/**
* @see DATAREST-221
*/
@Test
public void findsRegisteredProjection() {
ProjectionDefinitionConfiguration configuration = new ProjectionDefinitionConfiguration();
configuration.addProjection(Integer.class, "name", String.class);
assertThat(configuration.getProjectionType(String.class, "name"), is(equalTo((Class) Integer.class)));
}
/**
* @see DATAREST-221
*/
@Test
public void registersAnnotatedProjection() {
ProjectionDefinitionConfiguration configuration = new ProjectionDefinitionConfiguration();
configuration.addProjection(SampleProjection.class);
assertThat(configuration.getProjectionType(Integer.class, "name"), is(equalTo((Class) SampleProjection.class)));
}
/**
* @see DATAREST-221
*/
@Test
public void defaultsNameToSimpleClassNameIfNotAnnotated() {
ProjectionDefinitionConfiguration configuration = new ProjectionDefinitionConfiguration();
configuration.addProjection(Default.class);
assertThat(configuration.getProjectionType(Integer.class, "default"), is(equalTo((Class) Default.class)));
}
/**
* @see DATAREST-221
*/
@Test
public void definitionKeyEquals() {
ProjectionDefinitionKey objectNameKey = new ProjectionDefinitionKey(Object.class, "name");
ProjectionDefinitionKey sameObjectNameKey = new ProjectionDefinitionKey(Object.class, "name");
ProjectionDefinitionKey stringNameKey = new ProjectionDefinitionKey(String.class, "name");
ProjectionDefinitionKey objectOtherNameKey = new ProjectionDefinitionKey(Object.class, "otherName");
assertThat(objectNameKey, is(objectNameKey));
assertThat(objectNameKey, is(sameObjectNameKey));
assertThat(sameObjectNameKey, is(objectNameKey));
assertThat(objectNameKey, is(not(stringNameKey)));
assertThat(stringNameKey, is(not(objectNameKey)));
assertThat(objectNameKey, is(not(objectOtherNameKey)));
assertThat(objectOtherNameKey, is(not(objectNameKey)));
}
@Projection(name = "name", types = Integer.class)
interface SampleProjection {
}
@Projection(types = Integer.class)
interface Default {
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
/**
* Unit tests for {@link ProjectingMethodInterceptor}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class ProjectingMethodInterceptorUnitTests {
@Mock MethodInterceptor interceptor;
@Mock MethodInvocation invocation;
@Mock ProjectionFactory factory;
/**
* @see DATAREST-221
*/
@Test
public void wrapsDelegateResultInProxyIfTypesDontMatch() throws Throwable {
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(null), interceptor);
when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getHelper"));
when(interceptor.invoke(invocation)).thenReturn("Foo");
assertThat(methodInterceptor.invoke(invocation), is(instanceOf(Helper.class)));
}
/**
* @see DATAREST-221
*/
@Test
public void retunsDelegateResultAsIsIfTypesMatch() throws Throwable {
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor);
when(invocation.getMethod()).thenReturn(Helper.class.getMethod("getString"));
when(interceptor.invoke(invocation)).thenReturn("Foo");
assertThat(methodInterceptor.invoke(invocation), is((Object) "Foo"));
}
/**
* @see DATAREST-221
*/
@Test
public void returnsNullAsIs() throws Throwable {
MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(factory, interceptor);
when(interceptor.invoke(invocation)).thenReturn(null);
assertThat(methodInterceptor.invoke(invocation), is(nullValue()));
}
interface Helper {
Helper getHelper();
String getString();
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.NotReadablePropertyException;
/**
* Unit tests for {@link PropertyAccessingMethodInterceptor}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class PropertyAccessingMethodInterceptorUnitTests {
@Mock MethodInvocation invocation;
/**
* @see DATAREST-221
*/
@Test
public void triggersPropertyAccessOnTarget() throws Throwable {
Source source = new Source();
source.firstname = "Dave";
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getFirstname"));
MethodInterceptor interceptor = new PropertyAccessingMethodInterceptor(source);
assertThat(interceptor.invoke(invocation), is((Object) "Dave"));
}
/**
* @see DATAREST-221
*/
@Test(expected = NotReadablePropertyException.class)
public void throwsAppropriateExceptionIfThePropertyCannotBeFound() throws Throwable {
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("getLastname"));
new PropertyAccessingMethodInterceptor(new Source()).invoke(invocation);
}
static class Source {
String firstname;
}
interface Projection {
String getFirstname();
String getLastname();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.aop.TargetClassAware;
/**
* Unit tests for {@link ProxyProjectionFactory}.
*
* @author Oliver Gierke
*/
public class ProxyProjectionFactoryUnitTests {
ProjectionFactory factory = new ProxyProjectionFactory(null);
/**
* @see DATAREST-221
*/
@Test
public void createsProjectingProxy() {
Customer customer = new Customer();
customer.firstname = "Dave";
customer.lastname = "Matthews";
customer.address = new Address();
customer.address.city = "New York";
customer.address.zipCode = "ZIP";
CustomerExcerpt excerpt = factory.createProjection(customer, CustomerExcerpt.class);
assertThat(excerpt, is(instanceOf(TargetClassAware.class)));
assertThat(excerpt.getFirstname(), is("Dave"));
assertThat(excerpt.getAddress().getZipCode(), is("ZIP"));
}
/**
* @see DATAREST-221
*/
@Test
public void proxyExposesTargetClassAware() {
assertThat(factory.createProjection(new Object(), CustomerExcerpt.class), is(instanceOf(TargetClassAware.class)));
}
/**
* @see DATAREST-221
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNonInterfacesAsProjectionTarget() {
factory.createProjection(new Object(), Object.class);
}
static class Customer {
String firstname, lastname;
Address address;
}
static class Address {
String zipCode, city;
}
interface CustomerExcerpt {
String getFirstname();
AddressExcerpt getAddress();
}
interface AddressExcerpt {
String getZipCode();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2014 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.data.rest.core.projection;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* Unit tests for {@link SpelEvaluatingMethodInterceptor}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class SpelEvaluatingMethodInterceptorUnitTests {
@Mock MethodInterceptor delegate;
@Mock MethodInvocation invocation;
/**
* @see DATAREST-221
*/
@Test
public void invokesMethodOnTarget() throws Throwable {
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("propertyFromTarget"));
MethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), null);
assertThat(interceptor.invoke(invocation), is((Object) "property"));
}
/**
* @see DATAREST-221
*/
@Test
public void invokesMethodOnBean() throws Throwable {
when(invocation.getMethod()).thenReturn(Projection.class.getMethod("invokeBean"));
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
factory.registerSingleton("someBean", new SomeBean());
SpelEvaluatingMethodInterceptor interceptor = new SpelEvaluatingMethodInterceptor(delegate, new Target(), factory);
assertThat(interceptor.invoke(invocation), is((Object) "value"));
}
interface Projection {
@Value("#{target.property}")
String propertyFromTarget();
@Value("#{@someBean.value}")
String invokeBean();
}
static class Target {
public String getProperty() {
return "property";
}
}
static class SomeBean {
public String getValue() {
return "value";
}
}
}

View File

@@ -25,10 +25,10 @@ import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.OptimisticLockingFailureException;
@@ -57,23 +57,18 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @author Oliver Gierke
*/
@SuppressWarnings({ "rawtypes" })
class AbstractRepositoryRestController implements MessageSourceAware, InitializingBean {
class AbstractRepositoryRestController implements MessageSourceAware {
private static final Logger LOG = LoggerFactory.getLogger(AbstractRepositoryRestController.class);
private final PersistentEntityResourceAssembler<Object> perAssembler;
@Autowired(required = false) private ValidationExceptionHandler handler;
@Autowired(required = false) private PlatformTransactionManager txMgr;
private MessageSource messageSource;
private PagedResourcesAssembler<Object> assembler;
private final PagedResourcesAssembler<Object> pagedResourcesAssembler;
private MessageSourceAccessor messageSourceAccessor;
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> assembler,
PersistentEntityResourceAssembler<Object> entityResourceAssembler) {
this.assembler = assembler;
this.perAssembler = entityResourceAssembler;
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> pagedResourcesAssembler) {
this.pagedResourcesAssembler = pagedResourcesAssembler;
}
/*
@@ -82,18 +77,7 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi
*/
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public void afterPropertiesSet() throws Exception {
// FIXME:
// if (null != txMgr) {
// txTmpl = new TransactionTemplate(txMgr);
// txTmpl.afterPropertiesSet();
// }
this.messageSourceAccessor = new MessageSourceAccessor(messageSource);
}
@ExceptionHandler({ NullPointerException.class })
@@ -135,7 +119,7 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi
public ResponseEntity handleRepositoryConstraintViolationException(Locale locale,
RepositoryConstraintViolationException rcve) {
return response(null, new RepositoryConstraintViolationExceptionMessage(rcve, messageSource, locale),
return response(null, new RepositoryConstraintViolationExceptionMessage(rcve, messageSourceAccessor),
HttpStatus.BAD_REQUEST);
}
@@ -216,33 +200,33 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi
}
@SuppressWarnings({ "unchecked" })
protected Resources resultToResources(Object result) {
protected Resources resultToResources(Object result, PersistentEntityResourceAssembler assembler) {
if (result instanceof Page) {
Page<Object> page = (Page<Object>) result;
return entitiesToResources(page, assembler);
} else if (result instanceof Iterable) {
return entitiesToResources((Iterable<Object>) result);
return entitiesToResources((Iterable<Object>) result, assembler);
} else if (null == result) {
return new Resources(EMPTY_RESOURCE_LIST);
} else {
Resource<Object> resource = perAssembler.toResource(result);
Resource<Object> resource = assembler.toResource(result);
return new Resources(Collections.singletonList(resource));
}
}
protected Resources<? extends Resource<Object>> entitiesToResources(Page<Object> page,
PagedResourcesAssembler<Object> assembler) {
return assembler.toResource(page, perAssembler);
PersistentEntityResourceAssembler assembler) {
return pagedResourcesAssembler.toResource(page, assembler);
}
protected Resources<Resource<Object>> entitiesToResources(Iterable<Object> entities) {
protected Resources<Resource<Object>> entitiesToResources(Iterable<Object> entities,
PersistentEntityResourceAssembler assembler) {
List<Resource<Object>> resources = new ArrayList<Resource<Object>>();
for (Object obj : entities) {
resources.add(obj == null ? null : perAssembler.toResource(obj));
resources.add(obj == null ? null : assembler.toResource(obj));
}
return new Resources<Resource<Object>>(resources);

View File

@@ -34,8 +34,8 @@ public class PersistentEntityResource<T> extends Resource<T> {
private final PersistentEntity<?, ?> entity;
public static <T> PersistentEntityResource<T> wrap(PersistentEntity<?, ?> entity, T obj) {
return new PersistentEntityResource<T>(entity, obj);
public static <T> PersistentEntityResource<T> wrap(PersistentEntity<?, ?> entity, T obj, Link selfLink) {
return new PersistentEntityResource<T>(entity, obj, selfLink);
}
public PersistentEntityResource(PersistentEntity<?, ?> entity, T content, Link... links) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -18,32 +18,39 @@ package org.springframework.data.rest.webmvc;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.util.Assert;
/**
* {@link ResourceAssembler} to create {@link PersistentEntityResource}s for arbitrary domain objects.
*
* @author Oliver Gierke
*/
public class PersistentEntityResourceAssembler<T> implements ResourceAssembler<T, PersistentEntityResource<T>> {
public class PersistentEntityResourceAssembler implements ResourceAssembler<Object, PersistentEntityResource<Object>> {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final Projector projector;
/**
* Creates a new {@link PersistentEntityResourceAssembler}.
*
* @param repositories must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param projections must not be {@literal null}.
*/
public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks) {
public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(projector, "PersistentEntityProjector must not be be null!");
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projector = projector;
}
/*
@@ -51,22 +58,34 @@ public class PersistentEntityResourceAssembler<T> implements ResourceAssembler<T
* @see org.springframework.hateoas.ResourceAssembler#toResource(java.lang.Object)
*/
@Override
public PersistentEntityResource<T> toResource(T instance) {
public PersistentEntityResource<Object> toResource(Object instance) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instance.getClass());
PersistentEntityResource<T> resource = PersistentEntityResource.wrap(entity, instance);
resource.add(getSelfLinkFor(instance));
return resource;
return PersistentEntityResource.wrap(entity, projector.project(instance), getSelfLinkFor(instance));
}
/**
* Creates the self link for the given domain instance.
*
* @param instance must be a managed entity, not {@literal null}.
* @return
*/
public Link getSelfLinkFor(Object instance) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instance.getClass());
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
}
BeanWrapper<?, Object> wrapper = BeanWrapper.create(instance, null);
Object id = wrapper.getProperty(entity.getIdProperty());
return entityLinks.linkForSingleResource(entity.getType(), id).withSelfRel();
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
@@ -37,11 +37,10 @@ public class RepositoryController extends AbstractRepositoryRestController {
private final ResourceMappings mappings;
@Autowired
public RepositoryController(PagedResourcesAssembler<Object> assembler,
PersistentEntityResourceAssembler<Object> perAssembler, Repositories repositories, EntityLinks entityLinks,
ResourceMappings mappings) {
public RepositoryController(PagedResourcesAssembler<Object> assembler, Repositories repositories,
EntityLinks entityLinks, ResourceMappings mappings) {
super(assembler, perAssembler);
super(assembler);
this.repositories = repositories;
this.entityLinks = entityLinks;

View File

@@ -72,7 +72,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
private static final String BASE_MAPPING = "/{repository}";
private final EntityLinks entityLinks;
private final PersistentEntityResourceAssembler<Object> perAssembler;
private final RepositoryRestConfiguration config;
private final ConversionService conversionService;
private final DomainObjectMerger domainObjectMerger;
@@ -82,13 +81,11 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@Autowired
public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config,
EntityLinks entityLinks, PagedResourcesAssembler<Object> assembler,
PersistentEntityResourceAssembler<Object> perAssembler,
@Qualifier("defaultConversionService") ConversionService conversionService, DomainObjectMerger domainObjectMerger) {
super(assembler, perAssembler);
super(assembler);
this.entityLinks = entityLinks;
this.perAssembler = perAssembler;
this.config = config;
this.conversionService = conversionService;
this.domainObjectMerger = domainObjectMerger;
@@ -105,8 +102,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@ResponseBody
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET)
public Resources<?> listEntities(final RootResourceInformation resourceInformation, Pageable pageable, Sort sort)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
public Resources<?> getCollectionResource(final RootResourceInformation resourceInformation, Pageable pageable,
Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException,
HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION);
@@ -133,7 +131,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
.withRel(searchMappings.getRel()));
}
Resources<?> resources = resultToResources(results);
Resources<?> resources = resultToResources(results, assembler);
resources.add(links);
return resources;
}
@@ -142,10 +140,11 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@SuppressWarnings({ "unchecked" })
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = {
"application/x-spring-data-compact+json", "text/uri-list" })
public Resources<?> listEntitiesCompact(final RootResourceInformation repoRequest, Pageable pageable, Sort sort)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
public Resources<?> getCollectionResourceCompact(RootResourceInformation repoRequest, Pageable pageable, Sort sort,
PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException,
HttpRequestMethodNotSupportedException {
Resources<?> resources = listEntities(repoRequest, pageable, sort);
Resources<?> resources = getCollectionResource(repoRequest, pageable, sort, assembler);
List<Link> links = new ArrayList<Link>(resources.getLinks());
for (Resource<?> resource : ((Resources<Resource<?>>) resources).getContent()) {
@@ -170,11 +169,12 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@ResponseBody
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST)
public ResponseEntity<ResourceSupport> postEntity(RootResourceInformation resourceInformation,
PersistentEntityResource<?> payload) throws HttpRequestMethodNotSupportedException {
PersistentEntityResource<?> payload, PersistentEntityResourceAssembler assembler)
throws HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION);
return createAndReturn(payload.getContent(), resourceInformation.getInvoker());
return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler);
}
/**
@@ -186,8 +186,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @throws HttpRequestMethodNotSupportedException
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET)
public ResponseEntity<Resource<?>> getSingleEntity(RootResourceInformation resourceInformation,
@PathVariable String id) throws HttpRequestMethodNotSupportedException {
public ResponseEntity<Resource<?>> getItemResource(RootResourceInformation resourceInformation,
@PathVariable String id, PersistentEntityResourceAssembler assembler)
throws HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM);
@@ -203,7 +204,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
return new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Resource<?>>(perAssembler.toResource(domainObj), HttpStatus.OK);
return new ResponseEntity<Resource<?>>(assembler.toResource(domainObj), HttpStatus.OK);
}
/**
@@ -217,7 +218,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT)
public ResponseEntity<? extends ResourceSupport> putEntity(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException {
PersistentEntityResource<Object> payload, @PathVariable String id, PersistentEntityResourceAssembler assembler)
throws HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM);
@@ -229,10 +231,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
BeanWrapper<?, Object> incomingWrapper = BeanWrapper.create(payload.getContent(), conversionService);
incomingWrapper.setProperty(payload.getPersistentEntity().getIdProperty(), id);
return createAndReturn(incomingWrapper.getBean(), invoker);
return createAndReturn(incomingWrapper.getBean(), invoker, assembler);
}
return mergeAndReturn(payload.getContent(), domainObject, invoker, PUT);
return mergeAndReturn(payload.getContent(), domainObject, invoker, PUT, assembler);
}
/**
@@ -247,8 +249,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH)
public ResponseEntity<ResourceSupport> patchEntity(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException,
ResourceNotFoundException {
PersistentEntityResource<Object> payload, @PathVariable String id, PersistentEntityResourceAssembler assembler)
throws HttpRequestMethodNotSupportedException, ResourceNotFoundException {
resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM);
@@ -258,7 +260,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
throw new ResourceNotFoundException();
}
return mergeAndReturn(payload.getContent(), domainObject, resourceInformation.getInvoker(), PATCH);
return mergeAndReturn(payload.getContent(), domainObject, resourceInformation.getInvoker(), PATCH, assembler);
}
/**
@@ -304,7 +306,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @return
*/
private ResponseEntity<ResourceSupport> mergeAndReturn(Object incoming, Object domainObject,
RepositoryInvoker invoker, HttpMethod httpMethod) {
RepositoryInvoker invoker, HttpMethod httpMethod, PersistentEntityResourceAssembler assembler) {
NullHandlingPolicy nullPolicy = httpMethod.equals(PATCH) ? IGNORE_NULLS : APPLY_NULLS;
domainObjectMerger.merge(incoming, domainObject, nullPolicy);
@@ -316,11 +318,11 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
HttpHeaders headers = new HttpHeaders();
if (PUT.equals(httpMethod)) {
headers.setLocation(URI.create(perAssembler.getSelfLinkFor(obj).getHref()));
headers.setLocation(URI.create(assembler.getSelfLinkFor(obj).getHref()));
}
if (config.isReturnBodyOnUpdate()) {
return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, perAssembler.toResource(obj));
return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, assembler.toResource(obj));
} else {
return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers);
}
@@ -333,16 +335,17 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @param invoker
* @return
*/
private ResponseEntity<ResourceSupport> createAndReturn(Object domainObject, RepositoryInvoker invoker) {
private ResponseEntity<ResourceSupport> createAndReturn(Object domainObject, RepositoryInvoker invoker,
PersistentEntityResourceAssembler assembler) {
publisher.publishEvent(new BeforeCreateEvent(domainObject));
Object savedObject = invoker.invokeSave(domainObject);
publisher.publishEvent(new AfterCreateEvent(savedObject));
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(perAssembler.getSelfLinkFor(savedObject).getHref()));
headers.setLocation(URI.create(assembler.getSelfLinkFor(savedObject).expand().getHref()));
PersistentEntityResource<Object> resource = config.isReturnBodyOnCreate() ? perAssembler.toResource(savedObject)
PersistentEntityResource<Object> resource = config.isReturnBodyOnCreate() ? assembler.toResource(savedObject)
: null;
return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource);
}

View File

@@ -74,7 +74,6 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
private static final String BASE_MAPPING = "/{repository}/{id}/{property}";
private final Repositories repositories;
private final PersistentEntityResourceAssembler<Object> perAssembler;
private final ConversionService conversionService;
private ApplicationEventPublisher publisher;
@@ -82,12 +81,11 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@Autowired
public RepositoryPropertyReferenceController(Repositories repositories,
@Qualifier("defaultConversionService") ConversionService conversionService,
PagedResourcesAssembler<Object> assembler, PersistentEntityResourceAssembler<Object> perAssembler) {
PagedResourcesAssembler<Object> assembler) {
super(assembler, perAssembler);
super(assembler);
this.repositories = repositories;
this.perAssembler = perAssembler;
this.conversionService = conversionService;
}
@@ -102,7 +100,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET)
public ResponseEntity<ResourceSupport> followPropertyReference(final RootResourceInformation repoRequest,
@PathVariable String id, @PathVariable String property) throws Exception {
@PathVariable String id, @PathVariable String property, final PersistentEntityResourceAssembler assembler)
throws Exception {
final HttpHeaders headers = new HttpHeaders();
@@ -120,7 +119,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
List<Resource<?>> resources = new ArrayList<Resource<?>>();
for (Object obj : (Iterable<Object>) prop.propertyValue) {
resources.add(perAssembler.toResource(obj));
resources.add(assembler.toResource(obj));
}
return new Resources<Resource<?>>(resources);
@@ -130,14 +129,14 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
Map<Object, Resource<?>> resources = new HashMap<Object, Resource<?>>();
for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) prop.propertyValue).entrySet()) {
resources.put(entry.getKey(), perAssembler.toResource(entry.getValue()));
resources.put(entry.getKey(), assembler.toResource(entry.getValue()));
}
return new Resource<Object>(resources);
} else {
PersistentEntityResource<Object> resource = perAssembler.toResource(prop.propertyValue);
PersistentEntityResource<Object> resource = assembler.toResource(prop.propertyValue);
headers.set("Content-Location", resource.getId().getHref());
return resource;
}
@@ -190,7 +189,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.GET)
public ResponseEntity<ResourceSupport> followPropertyReference(final RootResourceInformation repoRequest,
@PathVariable String id, @PathVariable String property, final @PathVariable String propertyId) throws Exception {
@PathVariable String id, @PathVariable String property, final @PathVariable String propertyId,
final PersistentEntityResourceAssembler assembler) throws Exception {
final HttpHeaders headers = new HttpHeaders();
@@ -210,7 +210,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
if (propertyId.equals(sId)) {
PersistentEntityResource<Object> resource = perAssembler.toResource(obj);
PersistentEntityResource<Object> resource = assembler.toResource(obj);
headers.set("Content-Location", resource.getId().getHref());
return resource;
}
@@ -223,7 +223,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
if (propertyId.equals(sId)) {
PersistentEntityResource<Object> resource = perAssembler.toResource(entry.getValue());
PersistentEntityResource<Object> resource = assembler.toResource(entry.getValue());
headers.set("Content-Location", resource.getId().getHref());
return resource;
}
@@ -242,9 +242,10 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = {
"application/x-spring-data-compact+json", "text/uri-list" })
public ResponseEntity<ResourceSupport> followPropertyReferenceCompact(RootResourceInformation repoRequest,
@PathVariable String id, @PathVariable String property) throws Exception {
@PathVariable String id, @PathVariable String property, PersistentEntityResourceAssembler assembler)
throws Exception {
ResponseEntity<ResourceSupport> response = followPropertyReference(repoRequest, id, property);
ResponseEntity<ResourceSupport> response = followPropertyReference(repoRequest, id, property, assembler);
if (response.getStatusCode() != HttpStatus.OK) {
return response;
@@ -259,7 +260,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
List<Link> links = new ArrayList<Link>();
ControllerLinkBuilder linkBuilder = linkTo(methodOn(RepositoryPropertyReferenceController.class)
.followPropertyReference(repoRequest, id, property));
.followPropertyReference(repoRequest, id, property, assembler));
if (resource instanceof Resource) {

View File

@@ -67,18 +67,17 @@ class RepositorySearchController extends AbstractRepositoryRestController {
/**
* Creates a new {@link RepositorySearchController} using the given {@link PagedResourcesAssembler},
* {@link PersistentEntityResourceAssembler}, {@link EntityLinks} and {@link ResourceMappings}.
* {@link EntityLinks} and {@link ResourceMappings}.
*
* @param assembler must not be {@literal null}.
* @param perAssembler must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/
@Autowired
public RepositorySearchController(PagedResourcesAssembler<Object> assembler,
PersistentEntityResourceAssembler<Object> perAssembler, EntityLinks entityLinks, ResourceMappings mappings) {
public RepositorySearchController(PagedResourcesAssembler<Object> assembler, EntityLinks entityLinks,
ResourceMappings mappings) {
super(assembler, perAssembler);
super(assembler);
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
@@ -129,10 +128,10 @@ class RepositorySearchController extends AbstractRepositoryRestController {
@ResponseBody
@RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET)
public ResponseEntity<Resources<?>> executeSearch(RootResourceInformation resourceInformation, WebRequest request,
@PathVariable String search, Pageable pageable) {
@PathVariable String search, Pageable pageable, PersistentEntityResourceAssembler assembler) {
Method method = checkExecutability(resourceInformation, search);
Resources<?> resources = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable);
Resources<?> resources = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, assembler);
return new ResponseEntity<Resources<?>>(resources, HttpStatus.OK);
}
@@ -150,10 +149,12 @@ class RepositorySearchController extends AbstractRepositoryRestController {
@RequestMapping(value = BASE_MAPPING + "/{method}", method = RequestMethod.GET, //
produces = { "application/x-spring-data-compact+json" })
public ResourceSupport executeSearchCompact(RootResourceInformation resourceInformation, WebRequest request,
@PathVariable String repository, @PathVariable String search, Pageable pageable) {
@PathVariable String repository, @PathVariable String search, Pageable pageable,
PersistentEntityResourceAssembler assembler) {
Method method = checkExecutability(resourceInformation, search);
ResourceSupport resource = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable);
ResourceSupport resource = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable,
assembler);
List<Link> links = new ArrayList<Link>();
@@ -209,12 +210,12 @@ class RepositorySearchController extends AbstractRepositoryRestController {
* @return
*/
private Resources<?> executeQueryMethod(final RepositoryInvoker invoker, WebRequest request, Method method,
Pageable pageable) {
Pageable pageable, PersistentEntityResourceAssembler assembler) {
Map<String, String[]> parameters = request.getParameterMap();
Object result = invoker.invokeQueryMethod(method, parameters, pageable, null);
return resultToResources(result);
return resultToResources(result, assembler);
}
/**

View File

@@ -34,7 +34,7 @@ import org.springframework.web.HttpRequestMethodNotSupportedException;
* @author Jon Brisbin
* @author Oliver Gierke
*/
class RootResourceInformation {
public class RootResourceInformation {
private final ResourceMetadata resourceMetadata;
private final RepositoryInvoker invoker;

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2014 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.data.rest.webmvc.config;
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.util.Assert;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* {@link HandlerMethodArgumentResolver} to create {@link PersistentEntityResourceAssembler}s.
*
* @author Oliver Gierke
*/
public class PersistentEntityResourceAssemblerArgumentResolver implements HandlerMethodArgumentResolver {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
/**
* Creates a new {@link PersistentEntityResourceAssemblerArgumentResolver} for the given {@link Repositories},
* {@link EntityLinks}, {@link ProjectionDefinitions} and {@link ProjectionFactory}.
*
* @param repositories must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param projectionDefinitions must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
*/
public PersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!");
Assert.notNull(projectionFactory, "ProjectionFactory must not be null!");
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return PersistentEntityResourceAssembler.class.equals(parameter.getParameterType());
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter);
return new PersistentEntityResourceAssembler(repositories, entityLinks, projector);
}
}

View File

@@ -13,13 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
package org.springframework.data.rest.webmvc.config;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;

View File

@@ -18,7 +18,9 @@ package org.springframework.data.rest.webmvc.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectFactory;
@@ -34,9 +36,12 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.env.Environment;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.Projection;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AnnotatedHandlerBeanPostProcessor;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
@@ -44,15 +49,12 @@ import org.springframework.data.rest.core.invoke.DefaultRepositoryInvokerFactory
import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.util.UUIDConverter;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
import org.springframework.data.rest.webmvc.ResourceMetadataHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.RootResourceInformationHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver;
import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter;
import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper;
@@ -62,6 +64,9 @@ import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgum
import org.springframework.data.rest.webmvc.support.JpaHelper;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.data.rest.webmvc.support.ValidationExceptionHandler;
import org.springframework.data.util.AnnotatedTypeScanner;
import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver;
import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver;
import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.EntityLinks;
@@ -123,6 +128,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
RepositoryRestMvcConfiguration.class.getClassLoader());
@Autowired ListableBeanFactory beanFactory;
@Autowired Environment environment;
@Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
@Autowired(required = false) RelProvider relProvider;
@Autowired(required = false) CurieProvider curieProvider;
@@ -187,7 +194,14 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
*/
@Bean
public RepositoryRestConfiguration config() {
RepositoryRestConfiguration config = new RepositoryRestConfiguration();
ProjectionDefinitionConfiguration configuration = new ProjectionDefinitionConfiguration();
for (Class<?> projection : getProjections()) {
configuration.addProjection(projection);
}
RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration);
configureRepositoryRestConfiguration(config);
return config;
}
@@ -275,7 +289,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() {
return new PersistentEntityToJsonSchemaConverter(repositories(), resourceMappings(),
resourceDescriptionMessageSourceAccessor());
resourceDescriptionMessageSourceAccessor(), entityLinks());
}
/**
@@ -372,11 +386,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return new UriListHttpMessageConverter();
}
@Bean
public PersistentEntityResourceAssembler<Object> persistentEntityResourceAssembler() {
return new PersistentEntityResourceAssembler<Object>(repositories(), entityLinks());
}
/**
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
* provided controller classes.
@@ -472,10 +481,45 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return messageConverters;
}
/*
* (non-Javadoc)
* @see org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration#pageableResolver()
*/
@Bean
@Override
public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
HateoasPageableHandlerMethodArgumentResolver resolver = super.pageableResolver();
resolver.setPageParameterName(config().getPageParamName());
resolver.setSizeParameterName(config().getLimitParamName());
return resolver;
}
/*
* (non-Javadoc)
* @see org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration#sortResolver()
*/
@Bean
@Override
public HateoasSortHandlerMethodArgumentResolver sortResolver() {
HateoasSortHandlerMethodArgumentResolver resolver = super.sortResolver();
resolver.setSortParameter(config().getSortParamName());
return resolver;
}
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE);
PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));
return Arrays
.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver);
}
private ObjectMapper basicObjectMapper() {
@@ -496,6 +540,18 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return this.relProvider != null ? relProvider : new EvoInflectorRelProvider();
}
@SuppressWarnings("unchecked")
private Set<Class<?>> getProjections() {
Set<String> packagesToScan = new HashSet<String>();
for (Class<?> domainType : repositories()) {
packagesToScan.add(domainType.getPackage().getName());
}
return new AnnotatedTypeScanner(Projection.class).findTypes(packagesToScan);
}
/**
* Override this method to add additional configuration.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 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.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
package org.springframework.data.rest.webmvc.config;
import static org.springframework.util.ClassUtils.*;
import static org.springframework.util.StringUtils.*;
@@ -33,6 +33,8 @@ import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.UrlPathHelper;
/**
* {@link HandlerMethodArgumentResolver} to create {@link ResourceMetadata} instances.
*
* @author Jon Brisbin
* @author Oliver Gierke
*/
@@ -42,6 +44,9 @@ public class ResourceMetadataHandlerMethodArgumentResolver implements HandlerMet
private final ResourceMappings mappings;
/**
* Creates a new {@link ResourceMetadataHandlerMethodArgumentResolver} for the given {@link Repositories} and
* {@link ResourceMappings}.
*
* @param repositories must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
package org.springframework.data.rest.webmvc.config;
import org.springframework.core.MethodParameter;
import org.springframework.data.mapping.PersistentEntity;
@@ -21,6 +21,7 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.util.Assert;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

View File

@@ -29,15 +29,14 @@ import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.util.Assert;
@@ -99,12 +98,12 @@ public class PersistentEntityJackson2Module extends SimpleModule {
Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
addSerializer(new PersistentEntityResourceSerializer(mappings, config));
addSerializer(new PersistentEntityResourceSerializer(mappings));
setSerializerModifier(new AssociationOmittingSerializerModifier(repositories, mappings, config));
setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(repositories, converter, mappings));
}
public static boolean maybeAddAssociationLink(RepositoryLinkBuilder builder, ResourceMappings mappings,
public static boolean maybeAddAssociationLink(Path path, ResourceMappings mappings,
PersistentProperty<?> persistentProperty, List<Link> links) {
Assert.isTrue(persistentProperty.isAssociation(), "PersistentProperty must be an association!");
@@ -117,7 +116,8 @@ public class PersistentEntityJackson2Module extends SimpleModule {
ResourceMapping propertyMapping = ownerMetadata.getMappingFor(persistentProperty);
if (propertyMapping.isExported()) {
links.add(builder.slash(propertyMapping.getPath()).withRel(propertyMapping.getRel()));
links.add(new Link(path.slash(propertyMapping.getPath()).toString(), propertyMapping.getRel()));
// This is an association. We added a Link.
return true;
}
@@ -135,25 +135,20 @@ public class PersistentEntityJackson2Module extends SimpleModule {
private static class PersistentEntityResourceSerializer extends StdSerializer<PersistentEntityResource<?>> {
private final ResourceMappings mappings;
private final RepositoryRestConfiguration configuration;
/**
* Creates a new {@link PersistentEntityResourceSerializer} using the given {@link ResourceMappings} and
* {@link RepositoryRestConfiguration}.
* Creates a new {@link PersistentEntityResourceSerializer} using the given {@link ResourceMappings}.
*
* @param mappings must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private PersistentEntityResourceSerializer(ResourceMappings mappings, RepositoryRestConfiguration configuration) {
private PersistentEntityResourceSerializer(ResourceMappings mappings) {
super((Class) PersistentEntityResource.class);
Assert.notNull(mappings, "ResourceMappings must not be null!");
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
this.mappings = mappings;
this.configuration = configuration;
}
/*
@@ -168,19 +163,17 @@ public class PersistentEntityJackson2Module extends SimpleModule {
LOG.debug("Serializing PersistentEntity " + resource.getPersistentEntity());
}
Object obj = resource.getContent();
PersistentEntity<?, ?> entity = resource.getPersistentEntity();
BeanWrapper<PersistentEntity<Object, ?>, Object> wrapper = BeanWrapper.create(obj, null);
Object entityId = wrapper.getProperty(entity.getIdProperty());
ResourceMetadata metadata = mappings.getMappingFor(entity.getType());
URI baseUri = configuration.getBaseUri();
final Link id = resource.getId();
if (id == null) {
throw new JsonGenerationException(String.format("No self link found resource %s!", resource));
}
final RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, baseUri).slash(entityId);
final List<Link> links = new ArrayList<Link>();
links.addAll(resource.getLinks());
// Add associations as links
entity.doWithAssociations(new SimpleAssociationHandler() {
resource.getPersistentEntity().doWithAssociations(new SimpleAssociationHandler() {
/*
* (non-Javadoc)
@@ -190,11 +183,11 @@ public class PersistentEntityJackson2Module extends SimpleModule {
public void doWithAssociation(Association<? extends PersistentProperty<?>> association) {
PersistentProperty<?> property = association.getInverse();
maybeAddAssociationLink(builder, mappings, property, links);
maybeAddAssociationLink(new Path(id.expand().getHref()), mappings, property, links);
}
});
Resource<Object> resourceToRender = new Resource<Object>(obj, links);
Resource<Object> resourceToRender = new Resource<Object>(resource.getContent(), links);
provider.defaultSerializeValue(resourceToRender, jgen);
}
}

View File

@@ -34,13 +34,14 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.json.JsonSchema.ArrayProperty;
import org.springframework.data.rest.webmvc.json.JsonSchema.Property;
import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
@@ -57,6 +58,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
private final ResourceMappings mappings;
private final Repositories repositories;
private final MessageSourceAccessor accessor;
private final EntityLinks entityLinks;
/**
* Creates a new {@link PersistentEntityToJsonSchemaConverter} for the given {@link Repositories} and
@@ -67,7 +69,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
* @param accessor
*/
public PersistentEntityToJsonSchemaConverter(Repositories repositories, ResourceMappings mappings,
MessageSourceAccessor accessor) {
MessageSourceAccessor accessor, EntityLinks entityLinks) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
@@ -75,6 +77,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
this.repositories = repositories;
this.mappings = mappings;
this.accessor = accessor;
this.entityLinks = entityLinks;
for (Class<?> domainType : repositories) {
convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class));
@@ -111,7 +114,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity((Class<?>) source);
final PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity((Class<?>) source);
final ResourceMetadata metadata = mappings.getMappingFor(persistentEntity.getType());
final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), accessor.getMessage(metadata
.getItemResourceDescription()));
@@ -159,8 +162,8 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric
return;
}
RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, null).slash("{id}");
maybeAddAssociationLink(builder, mappings, persistentProperty, links);
Link link = entityLinks.linkToCollectionResource(persistentEntity.getType());
maybeAddAssociationLink(new Path(link.getHref()), mappings, persistentProperty, links);
}
});

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2014 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.data.rest.webmvc.support;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link Projector} looking up a projection by name for the given source type.
*
* @author Oliver Gierke
*/
public class PersistentEntityProjector implements Projector {
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory factory;
private final String projection;
/**
* Creates a new {@link PersistentEntityProjector} using the given {@link ProjectionDefinitions},
* {@link ProjectionFactory} and projection name.
*
* @param projectionDefinitions must not be {@literal null}.
* @param factory must not be {@literal null}.
* @param projection can be empty.
*/
public PersistentEntityProjector(ProjectionDefinitions projectionDefinitions, ProjectionFactory factory,
String projection) {
Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!");
Assert.notNull(factory, "ProjectionFactory must not be null!");
this.projectionDefinitions = projectionDefinitions;
this.factory = factory;
this.projection = projection;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.support.Projector#project(java.lang.Object)
*/
public Object project(Object source) {
if (!StringUtils.hasText(projection)) {
return source;
}
Class<?> projectionType = projectionDefinitions.getProjectionType(source.getClass(), projection);
return factory.createProjection(source, projectionType);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2014 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.data.rest.webmvc.support;
/**
* @author Oliver Gierke
*/
public interface Projector {
public Object project(Object source);
enum NoOpProjector implements Projector {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.rest.webmvc.support.Projector#project(java.lang.Object)
*/
@Override
public Object project(Object source) {
return source;
}
}
}

View File

@@ -2,9 +2,8 @@ package org.springframework.data.rest.webmvc.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.data.rest.core.RepositoryConstraintViolationException;
import org.springframework.validation.FieldError;
@@ -18,22 +17,23 @@ public class RepositoryConstraintViolationExceptionMessage {
private final List<ValidationError> errors = new ArrayList<ValidationError>();
public RepositoryConstraintViolationExceptionMessage(RepositoryConstraintViolationException violationException,
MessageSource msgSrc, Locale locale) {
MessageSourceAccessor accessor) {
for (FieldError fieldError : violationException.getErrors().getFieldErrors()) {
for (FieldError fe : violationException.getErrors().getFieldErrors()) {
List<Object> args = new ArrayList<Object>();
args.add(fe.getObjectName());
args.add(fe.getField());
args.add(fe.getRejectedValue());
if (null != fe.getArguments()) {
for (Object o : fe.getArguments()) {
args.add(fieldError.getObjectName());
args.add(fieldError.getField());
args.add(fieldError.getRejectedValue());
if (null != fieldError.getArguments()) {
for (Object o : fieldError.getArguments()) {
args.add(o);
}
}
String msg = msgSrc.getMessage(fe.getCode(), args.toArray(), fe.getDefaultMessage(), locale);
this.errors.add(new ValidationError(fe.getObjectName(), msg, String.format("%s", fe.getRejectedValue()), fe
.getField()));
String message = accessor.getMessage(fieldError.getCode(), args.toArray(), fieldError.getDefaultMessage());
this.errors.add(new ValidationError(fieldError.getObjectName(), message, String.format("%s",
fieldError.getRejectedValue()), fieldError.getField()));
}
}
@@ -43,10 +43,11 @@ public class RepositoryConstraintViolationExceptionMessage {
}
public static class ValidationError {
String entity;
String message;
String invalidValue;
String property;
private final String entity;
private final String message;
private final String invalidValue;
private final String property;
public ValidationError(String entity, String message, String invalidValue, String property) {
this.entity = entity;

View File

@@ -15,8 +15,11 @@
*/
package org.springframework.data.rest.webmvc.support;
import static org.springframework.hateoas.TemplateVariable.VariableType.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
@@ -24,6 +27,7 @@ import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.core.AbstractEntityLinks;
@@ -105,18 +109,22 @@ public class RepositoryEntityLinks extends AbstractEntityLinks {
public Link linkToCollectionResource(Class<?> type) {
ResourceMetadata metadata = mappings.getMappingFor(type);
TemplateVariables variables = new TemplateVariables();
String href = linkFor(type).withSelfRel().getHref();
if (metadata.isPagingResource()) {
Link link = linkFor(type).withSelfRel();
String href = link.getHref();
UriComponents components = UriComponentsBuilder.fromUriString(href).build();
TemplateVariables variables = resolver.getPaginationTemplateVariables(null, components);
return new Link(new UriTemplate(href, variables), metadata.getRel());
variables = variables.concat(resolver.getPaginationTemplateVariables(null, components));
}
return linkFor(type).withRel(metadata.getRel());
ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration();
if (projectionConfiguration.hasProjectionFor(type)) {
variables = variables.concat(new TemplateVariable(projectionConfiguration.getParameterName(), REQUEST_PARAM));
}
return variables.asList().isEmpty() ? linkFor(type).withRel(metadata.getRel()) : new Link(new UriTemplate(href,
variables), metadata.getRel());
}
/*
@@ -127,6 +135,17 @@ public class RepositoryEntityLinks extends AbstractEntityLinks {
public Link linkToSingleResource(Class<?> type, Object id) {
ResourceMetadata metadata = mappings.getMappingFor(type);
return linkFor(type).slash(id).withRel(metadata.getItemResourceRel());
Link link = linkFor(type).slash(id).withRel(metadata.getItemResourceRel());
ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration();
if (!projectionConfiguration.hasProjectionFor(type)) {
return link;
}
String parameterName = projectionConfiguration.getParameterName();
TemplateVariables templateVariables = new TemplateVariables(new TemplateVariable(parameterName, REQUEST_PARAM));
UriTemplate template = new UriTemplate(link.getHref(), templateVariables);
return new Link(template.toString(), metadata.getItemResourceRel());
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.rest.webmvc;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
@@ -25,6 +27,7 @@ import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -40,11 +43,20 @@ import org.springframework.web.context.request.WebRequest;
* @author Oliver Gierke
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { RepositoryRestMvcConfiguration.class })
@ContextConfiguration
public abstract class AbstractControllerIntegrationTests {
public static final Path BASE = new Path("http://localhost");
@Configuration
static class TestConfiguration extends RepositoryRestMvcConfiguration {
@Bean
public PersistentEntityResourceAssembler persistentEntityResourceAssembler() {
return new PersistentEntityResourceAssembler(repositories(), entityLinks(), Projector.NoOpProjector.INSTANCE);
}
}
@Autowired Repositories repositories;
@Autowired RepositoryInvokerFactory invokerFactory;
@Autowired ResourceMappings mappings;

View File

@@ -45,7 +45,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
repository.save(new Address());
RootResourceInformation request = getResourceInformation(Address.class);
controller.listEntities(request, null, null);
controller.getCollectionResource(request, null, null, null);
}
/**
@@ -56,6 +56,6 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
RootResourceInformation request = getResourceInformation(Address.class);
controller.postEntity(request, null);
controller.postEntity(request, null, null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -64,8 +64,8 @@ public class RepositoryRestHandlerMappingUnitTests {
mockRequest = new MockHttpServletRequest();
listEntitiesMethod = RepositoryEntityController.class.getMethod("listEntities", RootResourceInformation.class,
Pageable.class, Sort.class);
listEntitiesMethod = RepositoryEntityController.class.getMethod("getCollectionResource",
RootResourceInformation.class, Pageable.class, Sort.class, PersistentEntityResourceAssembler.class);
}
@Test(expected = IllegalArgumentException.class)

View File

@@ -46,6 +46,7 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
@Autowired TestDataPopulator loader;
@Autowired RepositorySearchController controller;
@Autowired PersistentEntityResourceAssembler assembler;
@Before
public void setUp() {
@@ -86,7 +87,7 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
RootResourceInformation resourceInformation = getResourceInformation(Person.class);
ResponseEntity<Resources<?>> response = controller.executeSearch(resourceInformation, getRequest(parameters),
"firstname", null);
"firstname", null, assembler);
ResourceTester tester = ResourceTester.of(response.getBody());
PagedResources<Object> pagedResources = tester.assertIsPage();

View File

@@ -40,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -430,6 +431,31 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
andExpect(status().isMethodNotAllowed());
}
/**
* Checks, that the server only returns the properties contained in the projection requested.
*
* @see OrderSummary
* @see DATAREST-221
*/
@Test
public void returnsProjectionIfRequested() throws Exception {
Link orders = discoverUnique("orders");
MockHttpServletResponse response = request(orders);
Link orderLink = assertContentLinkWithRel("self", response, true).expand();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(orderLink.getHref());
String uri = builder.queryParam("projection", "summary").build().toUriString();
response = mvc.perform(get(uri)). //
andExpect(status().isOk()). //
andExpect(jsonPath("$.price", is(2.5))).//
andReturn().getResponse();
assertJsonPathDoesntExist("$.lineItems", response);
}
/**
* Asserts the {@link Person} resource the given link points to contains siblings with the given names.
*

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2014 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.data.rest.webmvc.jpa;
import java.math.BigDecimal;
import org.springframework.data.rest.core.config.Projection;
/**
* @author Oliver Gierke
*/
@Projection(name = "summary", types = Order.class)
public interface OrderSummary {
BigDecimal getPrice();
}

View File

@@ -45,7 +45,6 @@ import org.springframework.hateoas.PagedResources.PageMetadata;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.UriTemplate;
@@ -114,8 +113,11 @@ public class PersistentEntitySerializationTests {
PersistentEntity<?, ?> persistentEntity = repositories.getPersistentEntity(Person.class);
Person person = people.save(new Person("John", "Doe"));
PersistentEntityResource<Person> resource = PersistentEntityResource.wrap(persistentEntity, person, new Link(
"/person/" + person.getId()));
StringWriter writer = new StringWriter();
mapper.writeValue(writer, PersistentEntityResource.wrap(persistentEntity, person));
mapper.writeValue(writer, resource);
String s = writer.toString();
@@ -180,14 +182,15 @@ public class PersistentEntitySerializationTests {
user.address.street = "Street";
PersistentEntityResource<User> userResource = new PersistentEntityResource<User>(
repositories.getPersistentEntity(User.class), user);
repositories.getPersistentEntity(User.class), user, new Link("/users/1"));
PagedResources<PersistentEntityResource<User>> persistentEntityResource = new PagedResources<PersistentEntityResource<User>>(
Arrays.asList(userResource), new PageMetadata(1, 0, 10));
assertThat(
mapper.writeValueAsString(persistentEntityResource),
is("{\"_embedded\":{\"users\":[{\"address\":{\"street\":\"Street\"}}]},\"page\":{\"size\":1,\"totalElements\":10,\"totalPages\":10,\"number\":0}}"));
is("{\"_embedded\":{\"users\":[{\"address\":{\"street\":\"Street\"},\"_links\":{\"self\":{\"href\":\"/users/1\"}}}]},"
+ "\"page\":{\"size\":1,\"totalElements\":10,\"totalPages\":10,\"number\":0}}"));
}
/**
@@ -199,12 +202,12 @@ public class PersistentEntitySerializationTests {
Person creator = new Person("Dave", "Matthews");
Order order = new Order(creator);
ReflectionTestUtils.setField(order, "id", 1L);
order.add(new LineItem("first"));
order.add(new LineItem("second"));
PersistentEntityResource<Order> orderResource = new PersistentEntityResource<Order>(
repositories.getPersistentEntity(Order.class), order);
orderResource.add(new Link("/orders/1"));
@SuppressWarnings("unchecked")
PagedResources<PersistentEntityResource<Order>> persistentEntityResource = new PagedResources<PersistentEntityResource<Order>>(
@@ -212,7 +215,7 @@ public class PersistentEntitySerializationTests {
assertThat(mapper.writeValueAsString(persistentEntityResource),
is("{\"_embedded\":{\"orders\":[{\"lineItems\":[{\"name\":\"first\"},{\"name\":\"second\"}],\"price\":2.5"
+ ",\"_links\":{\"creator\":{\"href\":\"http://localhost:8080/orders/1/creator\"}}}]},\""
+ ",\"_links\":{\"self\":{\"href\":\"/orders/1\"},\"creator\":{\"href\":\"/orders/1/creator\"}}}]},\""
+ "page\":{\"size\":1,\"totalElements\":10,\"totalPages\":10,\"number\":0}}"));
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2014 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.data.rest.webmvc.json;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
/**
* Integration tests for Jackson marshalling of projected objects.
*
* @author Oliver Gierke
*/
public class ProjectionJacksonIntegrationTests {
ObjectMapper mapper;
ProjectionFactory factory = new ProxyProjectionFactory(null);
@Before
public void setUp() {
this.mapper = new ObjectMapper();
this.mapper.registerModule(new Jackson2HalModule());
this.mapper.setHandlerInstantiator(new HalHandlerInstantiator(new EvoInflectorRelProvider(), null));
}
/**
* @see DATAREST-221
*/
@Test
public void considersJacksonAnnotationsOnProjectionInterfaces() throws Exception {
Customer customer = new Customer();
customer.firstname = "Dave";
customer.lastname = "Matthews";
customer.address = new Address();
CustomerProjection projection = factory.createProjection(customer, CustomerProjection.class);
String result = mapper.writeValueAsString(projection);
assertThat(JsonPath.read(result, "$firstname"), is((Object) "Dave"));
}
/**
* @see DATAREST-221
*/
@Test
public void rendersHalContentCorrectly() throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(new EvoInflectorRelProvider(), null));
Customer customer = new Customer();
customer.firstname = "Dave";
customer.lastname = "Matthews";
customer.address = new Address();
CustomerProjection projection = factory.createProjection(customer, CustomerProjection.class);
Resources<CustomerProjection> resources = new Resources<CustomerProjection>(Arrays.asList(projection));
String result = mapper.writeValueAsString(resources);
assertThat(JsonPath.read(result, "$_embedded.customers[0].firstname"), is((Object) "Dave"));
}
static class Customer {
String firstname, lastname;
Address address;
}
static class Address {
}
interface CustomerProjection {
String getFirstname();
@JsonIgnore
String getLastname();
}
}

View File

@@ -27,7 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
import org.springframework.hateoas.Link;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
/**
* Integration tests for MongoDB repositories.
@@ -94,7 +93,6 @@ public class MongoWebTests extends AbstractWebIntegrationTests {
Link usersLink = discoverUnique("users");
Link userLink = assertHasContentLinkWithRel("self", request(usersLink));
follow(userLink).//
andDo(MockMvcResultHandlers.print()). //
andExpect(jsonPath("$.address.zipCode").value(is(notNullValue())));
}
}

View File

@@ -20,8 +20,10 @@ import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.data.rest.webmvc.jpa.Order;
import org.springframework.data.rest.webmvc.jpa.Person;
import org.springframework.hateoas.Link;
import org.springframework.test.context.ContextConfiguration;
@@ -34,6 +36,7 @@ import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = JpaRepositoryConfig.class)
public class RepositoryEntityLinksIntegrationTests extends AbstractControllerIntegrationTests {
@Autowired RepositoryRestConfiguration configuration;
@Autowired RepositoryEntityLinks entityLinks;
@Test
@@ -54,4 +57,16 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt
assertThat(link.getVariableNames(), hasItems("page", "size", "sort"));
assertThat(link.getRel(), is("people"));
}
/**
* @see DATAREST-221
*/
@Test
public void returnsLinkWithProjectionTemplateVariableIfProjectionIsDefined() {
Link link = entityLinks.linkToSingleResource(Order.class, 1);
assertThat(link.isTemplated(), is(true));
assertThat(link.getVariableNames(), hasItem(configuration.projectionConfiguration().getParameterName()));
}
}