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-SNAPSHOT 1.0.0.RELEASE 1.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: + *
    + *
  1. Bean property accessor methods - invocations will be delegated into a property lookup on the target instance.
  2. + *
  3. 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.
  4. + *
+ * 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 perAssembler; - @Autowired(required = false) private ValidationExceptionHandler handler; @Autowired(required = false) private PlatformTransactionManager txMgr; - private MessageSource messageSource; - private PagedResourcesAssembler assembler; + private final PagedResourcesAssembler pagedResourcesAssembler; + private MessageSourceAccessor messageSourceAccessor; - public AbstractRepositoryRestController(PagedResourcesAssembler assembler, - PersistentEntityResourceAssembler entityResourceAssembler) { - - this.assembler = assembler; - this.perAssembler = entityResourceAssembler; + public AbstractRepositoryRestController(PagedResourcesAssembler 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 page = (Page) result; return entitiesToResources(page, assembler); } else if (result instanceof Iterable) { - return entitiesToResources((Iterable) result); + return entitiesToResources((Iterable) result, assembler); } else if (null == result) { return new Resources(EMPTY_RESOURCE_LIST); } else { - Resource resource = perAssembler.toResource(result); + Resource resource = assembler.toResource(result); return new Resources(Collections.singletonList(resource)); } } protected Resources> entitiesToResources(Page page, - PagedResourcesAssembler assembler) { - - return assembler.toResource(page, perAssembler); + PersistentEntityResourceAssembler assembler) { + return pagedResourcesAssembler.toResource(page, assembler); } - protected Resources> entitiesToResources(Iterable entities) { + protected Resources> entitiesToResources(Iterable entities, + PersistentEntityResourceAssembler assembler) { List> resources = new ArrayList>(); for (Object obj : entities) { - resources.add(obj == null ? null : perAssembler.toResource(obj)); + resources.add(obj == null ? null : assembler.toResource(obj)); } return new Resources>(resources); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java index b5b8eff01..5c9408698 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java @@ -34,8 +34,8 @@ public class PersistentEntityResource extends Resource { private final PersistentEntity entity; - public static PersistentEntityResource wrap(PersistentEntity entity, T obj) { - return new PersistentEntityResource(entity, obj); + public static PersistentEntityResource wrap(PersistentEntity entity, T obj, Link selfLink) { + return new PersistentEntityResource(entity, obj, selfLink); } public PersistentEntityResource(PersistentEntity entity, T content, Link... links) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java index e1e5b4e59..c97804de4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java @@ -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 implements ResourceAssembler> { +public class PersistentEntityResourceAssembler implements ResourceAssembler> { 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 implements ResourceAssembler toResource(T instance) { + public PersistentEntityResource toResource(Object instance) { PersistentEntity entity = repositories.getPersistentEntity(instance.getClass()); - - PersistentEntityResource 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 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 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); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index 979a0cb37..fa46d228b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.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. @@ -37,11 +37,10 @@ public class RepositoryController extends AbstractRepositoryRestController { private final ResourceMappings mappings; @Autowired - public RepositoryController(PagedResourcesAssembler assembler, - PersistentEntityResourceAssembler perAssembler, Repositories repositories, EntityLinks entityLinks, - ResourceMappings mappings) { + public RepositoryController(PagedResourcesAssembler assembler, Repositories repositories, + EntityLinks entityLinks, ResourceMappings mappings) { - super(assembler, perAssembler); + super(assembler); this.repositories = repositories; this.entityLinks = entityLinks; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 6a2e96972..656429fa6 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -72,7 +72,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem private static final String BASE_MAPPING = "/{repository}"; private final EntityLinks entityLinks; - private final PersistentEntityResourceAssembler 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 assembler, - PersistentEntityResourceAssembler 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 links = new ArrayList(resources.getLinks()); for (Resource resource : ((Resources>) resources).getContent()) { @@ -170,11 +169,12 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) public ResponseEntity 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> getSingleEntity(RootResourceInformation resourceInformation, - @PathVariable String id) throws HttpRequestMethodNotSupportedException { + public ResponseEntity> 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>(HttpStatus.NOT_FOUND); } - return new ResponseEntity>(perAssembler.toResource(domainObj), HttpStatus.OK); + return new ResponseEntity>(assembler.toResource(domainObj), HttpStatus.OK); } /** @@ -217,7 +218,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) public ResponseEntity putEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException { + PersistentEntityResource payload, @PathVariable String id, PersistentEntityResourceAssembler assembler) + throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -229,10 +231,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem BeanWrapper 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 patchEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException, - ResourceNotFoundException { + PersistentEntityResource 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 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 createAndReturn(Object domainObject, RepositoryInvoker invoker) { + private ResponseEntity 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 resource = config.isReturnBodyOnCreate() ? perAssembler.toResource(savedObject) + PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toResource(savedObject) : null; return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index b6fa8944c..d46665897 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -74,7 +74,6 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro private static final String BASE_MAPPING = "/{repository}/{id}/{property}"; private final Repositories repositories; - private final PersistentEntityResourceAssembler 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 assembler, PersistentEntityResourceAssembler perAssembler) { + PagedResourcesAssembler 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 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> resources = new ArrayList>(); for (Object obj : (Iterable) prop.propertyValue) { - resources.add(perAssembler.toResource(obj)); + resources.add(assembler.toResource(obj)); } return new Resources>(resources); @@ -130,14 +129,14 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro Map> resources = new HashMap>(); for (Map.Entry entry : ((Map) prop.propertyValue).entrySet()) { - resources.put(entry.getKey(), perAssembler.toResource(entry.getValue())); + resources.put(entry.getKey(), assembler.toResource(entry.getValue())); } return new Resource(resources); } else { - PersistentEntityResource resource = perAssembler.toResource(prop.propertyValue); + PersistentEntityResource 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 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 resource = perAssembler.toResource(obj); + PersistentEntityResource 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 resource = perAssembler.toResource(entry.getValue()); + PersistentEntityResource 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 followPropertyReferenceCompact(RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property) throws Exception { + @PathVariable String id, @PathVariable String property, PersistentEntityResourceAssembler assembler) + throws Exception { - ResponseEntity response = followPropertyReference(repoRequest, id, property); + ResponseEntity response = followPropertyReference(repoRequest, id, property, assembler); if (response.getStatusCode() != HttpStatus.OK) { return response; @@ -259,7 +260,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro List links = new ArrayList(); ControllerLinkBuilder linkBuilder = linkTo(methodOn(RepositoryPropertyReferenceController.class) - .followPropertyReference(repoRequest, id, property)); + .followPropertyReference(repoRequest, id, property, assembler)); if (resource instanceof Resource) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index 3b88eb48b..05e4dcaef 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -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 assembler, - PersistentEntityResourceAssembler perAssembler, EntityLinks entityLinks, ResourceMappings mappings) { + public RepositorySearchController(PagedResourcesAssembler 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> 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, 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 links = new ArrayList(); @@ -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 parameters = request.getParameterMap(); Object result = invoker.invokeQueryMethod(method, parameters, pageable, null); - return resultToResources(result); + return resultToResources(result, assembler); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java index 2d8174746..ccccb504e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java @@ -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; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java new file mode 100644 index 000000000..21efa127f --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java @@ -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); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java similarity index 96% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java index c45472580..236993ec9 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java @@ -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; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 7c0223745..4932ef165 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -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> 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 persistentEntityResourceAssembler() { - return new PersistentEntityResourceAssembler(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 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> getProjections() { + + Set packagesToScan = new HashSet(); + + for (Class domainType : repositories()) { + packagesToScan.add(domainType.getPackage().getName()); + } + + return new AnnotatedTypeScanner(Projection.class).findTypes(packagesToScan); + } + /** * Override this method to add additional configuration. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java similarity index 91% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java index cbcd2ca64..a374ecd7d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.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. @@ -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}. */ diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformationHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java similarity index 97% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformationHandlerMethodArgumentResolver.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java index b89a95517..d20a08719 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformationHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java @@ -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; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index b1843bbdc..44d596178 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -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 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> { 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, 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 links = new ArrayList(); 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> association) { PersistentProperty property = association.getInverse(); - maybeAddAssociationLink(builder, mappings, property, links); + maybeAddAssociationLink(new Path(id.expand().getHref()), mappings, property, links); } }); - Resource resourceToRender = new Resource(obj, links); + Resource resourceToRender = new Resource(resource.getContent(), links); provider.defaultSerializeValue(resourceToRender, jgen); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index c9040a69f..e05c7d5af 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -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); } }); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java new file mode 100644 index 000000000..5530f17c4 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java @@ -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); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java new file mode 100644 index 000000000..90bc90445 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java @@ -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; + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java index 98bc4f799..932e94f82 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryConstraintViolationExceptionMessage.java @@ -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 errors = new ArrayList(); public RepositoryConstraintViolationExceptionMessage(RepositoryConstraintViolationException violationException, - MessageSource msgSrc, Locale locale) { + MessageSourceAccessor accessor) { + + for (FieldError fieldError : violationException.getErrors().getFieldErrors()) { - for (FieldError fe : violationException.getErrors().getFieldErrors()) { List args = new ArrayList(); - 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; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 6ec0575c4..225d963fb 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -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()); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java index 2abd72ee8..ba5b8a553 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java @@ -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; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 32aeb7c73..401c65130 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -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); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java index 024a389b5..b3bb8837d 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java @@ -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) diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index cb6722630..f01a086c5 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -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> response = controller.executeSearch(resourceInformation, getRequest(parameters), - "firstname", null); + "firstname", null, assembler); ResourceTester tester = ResourceTester.of(response.getBody()); PagedResources pagedResources = tester.assertIsPage(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index f1a49c92c..3db199df8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -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. * diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java new file mode 100644 index 000000000..a3017f8ac --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderSummary.java @@ -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(); +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index 66daba460..12461432a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -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 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 userResource = new PersistentEntityResource( - repositories.getPersistentEntity(User.class), user); + repositories.getPersistentEntity(User.class), user, new Link("/users/1")); PagedResources> persistentEntityResource = new PagedResources>( 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 orderResource = new PersistentEntityResource( repositories.getPersistentEntity(Order.class), order); + orderResource.add(new Link("/orders/1")); @SuppressWarnings("unchecked") PagedResources> persistentEntityResource = new PagedResources>( @@ -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}}")); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/ProjectionJacksonIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/ProjectionJacksonIntegrationTests.java new file mode 100644 index 000000000..6a87c4636 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/ProjectionJacksonIntegrationTests.java @@ -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 resources = new Resources(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(); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java index eb4ed7a8f..aafd96b6a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java @@ -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()))); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java index 7e92eb01f..b87442518 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -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())); + } }