diff --git a/spring-data-rest-core/pom.xml b/spring-data-rest-core/pom.xml
index fb67104d3..1b06b2d67 100644
--- a/spring-data-rest-core/pom.xml
+++ b/spring-data-rest-core/pom.xml
@@ -16,7 +16,7 @@
- 0.9.0.RELEASE
+ 0.10.0.BUILD-SNAPSHOT1.0.0.RELEASE1.0.1
@@ -59,6 +59,12 @@
true
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson}
+
+
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/Projection.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/Projection.java
new file mode 100644
index 000000000..c6fd8ede0
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/Projection.java
@@ -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 "";
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java
new file mode 100644
index 000000000..9175fe344
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfiguration.java
@@ -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> projectionDefinitions;
+ private String parameterName = DEFAULT_PROJECTION_PARAMETER_NAME;
+
+ public ProjectionDefinitionConfiguration() {
+ this.projectionDefinitions = new HashMap>();
+ }
+
+ /*
+ * (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;
+ }
+ }
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java
index b493b720d..4a8edc748 100644
--- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java
@@ -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> exposeIdsFor = new ArrayList>();
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;
+ }
}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java
new file mode 100644
index 000000000..b2ac58fbb
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptor.java
@@ -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);
+ }
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionDefinitions.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionDefinitions.java
new file mode 100644
index 000000000..709736c06
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionDefinitions.java
@@ -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();
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionFactory.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionFactory.java
new file mode 100644
index 000000000..132e40302
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProjectionFactory.java
@@ -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 createProjection(Object source, Class projectionType);
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptor.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptor.java
new file mode 100644
index 000000000..4f2b6b9e1
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptor.java
@@ -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());
+ }
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProxyProjectionFactory.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProxyProjectionFactory.java
new file mode 100644
index 000000000..7ab28f450
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/ProxyProjectionFactory.java
@@ -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:
+ *
+ *
Bean property accessor methods - invocations will be delegated into a property lookup on the target instance.
+ *
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.
+ *
+ * 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, Boolean> typeCache = new HashMap, 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 createProjection(Object source, Class 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 callback = new AnnotationDetectionMethodCallback(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();
+ }
+ }
+}
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptor.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptor.java
new file mode 100644
index 000000000..2b8a9e542
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptor.java
@@ -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;
+ }
+ }
+}
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfigurationUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfigurationUnitTests.java
new file mode 100644
index 000000000..f89f08965
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/config/ProjectionDefinitionConfigurationUnitTests.java
@@ -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 {
+
+ }
+}
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java
new file mode 100644
index 000000000..a7a4a0468
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProjectingMethodInterceptorUnitTests.java
@@ -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();
+ }
+}
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptorUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptorUnitTests.java
new file mode 100644
index 000000000..d09eb1321
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/PropertyAccessingMethodInterceptorUnitTests.java
@@ -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();
+ }
+}
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProxyProjectionFactoryUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProxyProjectionFactoryUnitTests.java
new file mode 100644
index 000000000..9e4619fa8
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/ProxyProjectionFactoryUnitTests.java
@@ -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();
+ }
+}
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptorUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptorUnitTests.java
new file mode 100644
index 000000000..b42dee267
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/projection/SpelEvaluatingMethodInterceptorUnitTests.java
@@ -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";
+ }
+ }
+}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java
index 6ded121f0..6d4d8ba9b 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java
@@ -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