DATAREST-724 - Added support for customizing the property to be used for URI generation.

Spring Data REST now exposes an EntityLookup interface that allows to customize the property of an entity that shall be used to create item resource URIs. By default this mechanism uses the backend identifier and uses the repository's findOne(…) method.

The EntityLookup now exposes one method to return the property value to be used for URI generation as well as one method to obtain the entity instance from the very same raw identifier value. The EntityLookups are registered with both the SelfLinkProvider (for link creation) and the RepositoryInvoker (to obtain the entity instance).
This commit is contained in:
Oliver Gierke
2015-12-09 11:41:05 +01:00
parent e50a9dcb89
commit 44ab756873
17 changed files with 564 additions and 107 deletions

2
lombok.config Normal file
View File

@@ -0,0 +1,2 @@
lombok.nonNull.exceptionType = IllegalArgumentException
lombok.log.fieldName = LOG

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2015 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.support;
import java.util.List;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
/**
* Default implementation of SelfLinkProvider that uses an {@link EntityLinks} instance to create self links. Considers
* the configured {@link EntityLookup}s to use the returned resource identifier to eventually create the link.
*
* @author Oliver Gierke
* @since 2.5
* @soundtrack Trio Rotation - Travis
*/
public class DefaultSelfLinkProvider implements SelfLinkProvider {
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final PluginRegistry<EntityLookup<?>, Class<?>> lookups;
/**
* Creates a new {@link DefaultSelfLinkProvider} from the {@link PersistentEntities}, {@link EntityLinks} and
* {@link EntityLookup}s.
*
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param lookups must not be {@literal null}.
*/
public DefaultSelfLinkProvider(PersistentEntities entities, EntityLinks entityLinks,
List<? extends EntityLookup<?>> lookups) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(lookups, "EntityLookups must not be null!");
this.entities = entities;
this.entityLinks = entityLinks;
this.lookups = OrderAwarePluginRegistry.create(lookups);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.support.SelfLinkProvider#createSelfLinkFor(java.lang.Object)
*/
public Link createSelfLinkFor(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
return entityLinks.linkToSingleResource(instance.getClass(), getResourceId(instance));
}
/**
* Returns the identifier to be used to create the self link URI.
*
* @param instance must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private Object getResourceId(Object instance) {
Class<? extends Object> instanceType = instance.getClass();
EntityLookup<Object> lookup = (EntityLookup<Object>) lookups.getPluginFor(instanceType);
if (lookup != null) {
return lookup.getResourceIdentifier(instance);
}
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(
String.format("Cannot create self link for %s! No persistent entity found!", instanceType));
}
return entity.getIdentifierAccessor(instance).getIdentifier();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2015 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.support;
import java.io.Serializable;
import org.springframework.plugin.core.Plugin;
/**
* SPI to customize which property of an entity is used as unique identifier and how the entity instance is looked up
* from the backend. Prefer to extend {@link EntityLookupSupport} to let the generics declaration be used for the
* {@link #supports(Object)} method automatically.
*
* @author Oliver Gierke
* @see EntityLookupSupport
* @see DefaultSelfLinkProvider
* @since 2.5
* @soundtrack Elephants Crossing - Echo (Live at Stadtfest Dresden -
* https://soundcloud.com/elephants-crossing/sets/live-at-stadtfest-dresden)
*/
public interface EntityLookup<T> extends Plugin<Class<?>> {
/**
* Returns the property of the given entity that shall be used to uniquely identify it. If no {@link EntityLookup} is
* defined for a particular type, a standard identifier lookup mechanism (i.e. the datastore identifier) will be used
* to eventually create an identifying URI.
*
* @param entity will never be {@literal null}.
* @return must not be {@literal null}.
*/
Serializable getResourceIdentifier(T entity);
/**
* Returns the entity instance to be used if an entity with the given identifier value is requested. Implementations
* will usually forward the call to a repository method explicitly and can assume the given value be basically the
* value they returned in {@link #getResourceIdentifier(Object)}.
* <p>
* Implementations are free to return {@literal null} to indicate absence of a value or wrap the result into any
* generally supported {@code Optional} type.
*
* @param id will never be {@literal null}.
* @return can be {@literal null}.
*/
Object lookupEntity(Serializable id);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2015 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.support;
import org.springframework.core.GenericTypeResolver;
/**
* {@link EntityLookup} implementation base class to derive the supported domain type from the generics signature.
*
* @author Oliver Gierke
* @since 2.5
* @soundtrack Elephants Crossing - The New (Live at Stadtfest Dresden -
* https://soundcloud.com/elephants-crossing/sets/live-at-stadtfest-dresden)
*/
public abstract class EntityLookupSupport<T> implements EntityLookup<T> {
private final Class<?> domainType;
/**
* Creates a new {@link EntityLookupSupport} instance discovering the supported type from the generics signature.
*/
public EntityLookupSupport() {
this.domainType = GenericTypeResolver.resolveTypeArgument(getClass(), EntityLookup.class);
}
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/
@Override
public boolean supports(Class<?> delimiter) {
return domainType.isAssignableFrom(delimiter);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2015 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.support;
import org.springframework.hateoas.Link;
/**
* Component to create self links for entity instances.
*
* @author Oliver Gierke
* @since 2.5
* @soundtrack Trio Rotation - Rotation
*/
public interface SelfLinkProvider {
/**
* Returns the self link for the given entity instance.
*
* @param instance must never be {@literal null}.
* @return will never be {@literal null}.
*/
Link createSelfLinkFor(Object instance);
}

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.rest.core.support;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -29,6 +32,8 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
@@ -66,8 +71,8 @@ public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFact
converters.add(new Converter<Object, Object>() {
@Override
public Object convert(Object source) {
return source instanceof com.google.common.base.Optional ? ((com.google.common.base.Optional<?>) source)
.orNull() : source;
return source instanceof com.google.common.base.Optional
? ((com.google.common.base.Optional<?>) source).orNull() : source;
}
});
}
@@ -76,17 +81,20 @@ public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFact
}
private final RepositoryInvokerFactory delegate;
private final PluginRegistry<EntityLookup<?>, Class<?>> lookups;
/**
* Creates a new {@link UnwrappingRepositoryInvokerFactory}.
*
* @param delegate must not be {@literal null}.
* @param lookups must not be {@literal null}.
*/
public UnwrappingRepositoryInvokerFactory(RepositoryInvokerFactory delegate) {
public UnwrappingRepositoryInvokerFactory(RepositoryInvokerFactory delegate,
List<? extends EntityLookup<?>> lookups) {
Assert.notNull(delegate, "Delegate RepositoryInvokerFactory must not be null!");
Assert.notNull(lookups, "EntityLookups must not be null!");
this.delegate = delegate;
this.lookups = OrderAwarePluginRegistry.create(lookups);
}
/*
@@ -95,7 +103,10 @@ public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFact
*/
@Override
public RepositoryInvoker getInvokerFor(Class<?> domainType) {
return new UnwrappingRepositoryInvoker(delegate.getInvokerFor(domainType), CONVERTERS);
EntityLookup<?> lookup = lookups.getPluginFor(domainType);
return new UnwrappingRepositoryInvoker(delegate.getInvokerFor(domainType), CONVERTERS, lookup);
}
/**
@@ -104,33 +115,20 @@ public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFact
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
private static class UnwrappingRepositoryInvoker implements RepositoryInvoker {
private final RepositoryInvoker delegate;
private final Collection<Converter<Object, Object>> converters;
/**
* Creates a new {@link UnwrappingRepositoryInvoker} for the given delegate and {@link Converter}s.
*
* @param delegate must not be {@literal null}.
* @param converters must not be {@literal null}.
*/
public UnwrappingRepositoryInvoker(RepositoryInvoker delegate, Collection<Converter<Object, Object>> converters) {
Assert.notNull(delegate, "Delegate RepositoryInvoker must not be null!");
Assert.notNull(converters, "Converters must not be null!");
this.delegate = delegate;
this.converters = converters;
}
private final @NonNull RepositoryInvoker delegate;
private final @NonNull Collection<Converter<Object, Object>> converters;
private final EntityLookup<?> lookup;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.support.RepositoryInvoker#invokeFindOne(java.io.Serializable)
*/
@SuppressWarnings("unchecked")
public <T> T invokeFindOne(Serializable id) {
return (T) postProcess(delegate.invokeFindOne(id));
return postProcess(lookup != null ? lookup.lookupEntity(id) : delegate.invokeFindOne(id));
}
/*
@@ -231,13 +229,14 @@ public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFact
* @param result can be {@literal null}.
* @return
*/
private Object postProcess(Object result) {
@SuppressWarnings("unchecked")
private <T> T postProcess(Object result) {
for (Converter<Object, Object> converter : converters) {
result = converter.convert(result);
}
return result;
return (T) result;
}
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2015 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.support;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.rest.core.domain.mongodb.Profile;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
/**
* Unit tests for {@link DefaultSelfLinkProvider}.
*
* @author Oliver Gierke
* @soundtrack Trio Rotation - Triopane
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultSelfLinkProviderUnitTests {
SelfLinkProvider provider;
@Mock EntityLinks entityLinks;
PersistentEntities entities;
List<EntityLookup<?>> lookups;
public @Rule ExpectedException exception = ExpectedException.none();
@Before
public void setUp() {
when(entityLinks.linkToSingleResource((Class<?>) any(), any())).then(new Answer<Link>() {
@Override
public Link answer(InvocationOnMock invocation) throws Throwable {
Class<?> type = invocation.getArgumentAt(0, Class.class);
Serializable id = invocation.getArgumentAt(1, Serializable.class);
return new Link("/".concat(type.getName()).concat("/").concat(id.toString()));
}
});
MongoMappingContext context = new MongoMappingContext();
context.getPersistentEntity(Profile.class);
context.afterPropertiesSet();
this.entities = new PersistentEntities(Arrays.asList(context));
this.lookups = Collections.emptyList();
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntities() {
new DefaultSelfLinkProvider(null, entityLinks, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntityLinks() {
new DefaultSelfLinkProvider(entities, null, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntityLookups() {
new DefaultSelfLinkProvider(entities, entityLinks, null);
}
/**
* @see DATAREST-724
*/
@Test
public void usesEntityIdIfNoLookupDefined() {
String id = UUID.randomUUID().toString();
Link link = provider.createSelfLinkFor(new Profile(id, "Name", "Type"));
assertThat(link.getHref(), Matchers.endsWith(id));
}
/**
* @see DATAREST-724
*/
@Test
@SuppressWarnings("unchecked")
public void usesEntityLookupIfDefined() {
EntityLookup<Object> lookup = mock(EntityLookup.class);
when(lookup.supports(Profile.class)).thenReturn(true);
when(lookup.getResourceIdentifier(any(Profile.class))).thenReturn("foo");
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Arrays.asList(lookup));
String id = UUID.randomUUID().toString();
Link link = provider.createSelfLinkFor(new Profile(id, "Name", "Type"));
assertThat(link.getHref(), Matchers.endsWith("foo"));
}
/**
* @see DATAREST-724
*/
@Test
public void rejectsLinkCreationForUnknownEntity() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(Object.class.getName());
exception.expectMessage("No persistent entity found!");
provider.createSelfLinkFor(new Object());
}
}

View File

@@ -17,11 +17,13 @@ package org.springframework.data.rest.core.support;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import org.hamcrest.Matcher;
@@ -33,6 +35,7 @@ import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.domain.mongodb.Profile;
import org.springframework.util.LinkedMultiValueMap;
/**
@@ -59,7 +62,7 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests {
when(delegate.getInvokerFor(Object.class)).thenReturn(invoker);
this.factory = new UnwrappingRepositoryInvokerFactory(delegate);
this.factory = new UnwrappingRepositoryInvokerFactory(delegate, Collections.<EntityLookup<?>> emptyList());
this.method = Object.class.getMethod("toString");
}
@@ -89,6 +92,24 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests {
assertQueryValueForSource(source, value);
}
/**
* @see DATAREST-724
*/
@Test
@SuppressWarnings("unchecked")
public void usesRegisteredEntityLookup() {
EntityLookup<Object> lookup = mock(EntityLookup.class);
when(lookup.supports(Profile.class)).thenReturn(true);
when(delegate.getInvokerFor(Profile.class)).thenReturn(invoker);
factory = new UnwrappingRepositoryInvokerFactory(delegate, Arrays.asList(lookup));
factory.getInvokerFor(Profile.class).invokeFindOne(1L);
verify(lookup, times(1)).lookupEntity(eq(1L));
}
private void assertFindOneValueForSource(Object source, Matcher<Object> value) {
when(invoker.invokeFindOne(1L)).thenReturn(source);
@@ -98,8 +119,7 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests {
private void assertQueryValueForSource(Object source, Matcher<Object> value) {
when(invoker.invokeQueryMethod(method, new LinkedMultiValueMap<String, Object>(), null, null)).thenReturn(source);
assertThat(
factory.getInvokerFor(Object.class).invokeQueryMethod(method, new LinkedMultiValueMap<String, Object>(), null,
null), value);
assertThat(factory.getInvokerFor(Object.class).invokeQueryMethod(method, new LinkedMultiValueMap<String, Object>(),
null, null), value);
}
}

View File

@@ -27,10 +27,10 @@ import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.PersistentEntityResource.Builder;
import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
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.hateoas.core.EmbeddedWrapper;
@@ -45,29 +45,29 @@ import org.springframework.util.Assert;
public class PersistentEntityResourceAssembler implements ResourceAssembler<Object, PersistentEntityResource> {
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final Projector projector;
private final ResourceMappings mappings;
private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
private final SelfLinkProvider linkProvider;
/**
* Creates a new {@link PersistentEntityResourceAssembler}.
*
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param linkProvider must not be {@literal null}.
* @param projector must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/
public PersistentEntityResourceAssembler(PersistentEntities entities, EntityLinks entityLinks, Projector projector,
ResourceMappings mappings) {
public PersistentEntityResourceAssembler(PersistentEntities entities, SelfLinkProvider linkProvider,
Projector projector, ResourceMappings mappings) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(linkProvider, "SelfLinkProvider must not be null!");
Assert.notNull(projector, "PersistentEntityProjector must not be be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
this.entities = entities;
this.entityLinks = entityLinks;
this.linkProvider = linkProvider;
this.projector = projector;
this.mappings = mappings;
}
@@ -102,7 +102,7 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
return PersistentEntityResource.build(instance, entity).//
withEmbedded(getEmbeddedResources(source)).//
withLink(getSelfLinkFor(source)).//
withLink(getSingleResourceLinkTo(source));
withLink(linkProvider.createSelfLinkFor(source));
}
/**
@@ -184,23 +184,8 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler<Obje
* @return
*/
public Link getSelfLinkFor(Object instance) {
return new Link(getSingleResourceLinkTo(instance).expand().getHref(), Link.REL_SELF);
}
private Link getSingleResourceLinkTo(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = entities.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(
String.format("Cannot create self link for %s! No persistent entity found!", instanceType));
}
Object id = entity.getIdentifierAccessor(instance).getIdentifier();
return entityLinks.linkToSingleResource(entity.getType(), id);
Link link = linkProvider.createSelfLinkFor(instance);
return new Link(link.expand().getHref(), Link.REL_SELF);
}
}

View File

@@ -21,9 +21,10 @@ import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.SelfLinkProvider;
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;
@@ -38,30 +39,30 @@ import org.springframework.web.method.support.ModelAndViewContainer;
public class PersistentEntityResourceAssemblerArgumentResolver implements HandlerMethodArgumentResolver {
private final PersistentEntities entities;
private final EntityLinks entityLinks;
private final SelfLinkProvider linkProvider;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
private final ResourceMappings mappings;
/**
* Creates a new {@link PersistentEntityResourceAssemblerArgumentResolver} for the given {@link Repositories},
* {@link EntityLinks}, {@link ProjectionDefinitions} and {@link ProjectionFactory}.
* {@link DefaultSelfLinkProvider}, {@link ProjectionDefinitions} and {@link ProjectionFactory}.
*
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param linkProvider must not be {@literal null}.
* @param projectionDefinitions must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
*/
public PersistentEntityResourceAssemblerArgumentResolver(PersistentEntities entities, EntityLinks entityLinks,
public PersistentEntityResourceAssemblerArgumentResolver(PersistentEntities entities, SelfLinkProvider linkProvider,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory, ResourceMappings mappings) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(linkProvider, "EntityLinks must not be null!");
Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!");
Assert.notNull(projectionFactory, "ProjectionFactory must not be null!");
this.entities = entities;
this.entityLinks = entityLinks;
this.linkProvider = linkProvider;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
this.mappings = mappings;
@@ -88,6 +89,6 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter, mappings);
return new PersistentEntityResourceAssembler(entities, entityLinks, projector, mappings);
return new PersistentEntityResourceAssembler(entities, linkProvider, projector, mappings);
}
}

View File

@@ -67,8 +67,11 @@ import org.springframework.data.rest.core.event.ValidatingRepositoryEventListene
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.core.support.RepositoryRelProvider;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping;
@@ -162,6 +165,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Autowired(required = false) List<BackendIdConverter> idConverters = Collections.emptyList();
@Autowired(required = false) List<RepositoryRestConfigurer> configurers = Collections.emptyList();
@Autowired(required = false) List<EntityLookup<?>> lookups = Collections.emptyList();
@Autowired(required = false) RelProvider relProvider;
@Autowired(required = false) CurieProvider curieProvider;
@@ -588,7 +592,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
PersistentEntities entities = persistentEntities();
return new PersistentEntityJackson2Module(resourceMappings(), entities, config(),
uriToEntityConverter(defaultConversionService()), entityLinks());
uriToEntityConverter(defaultConversionService()), selfLinkProvider());
}
/**
@@ -624,8 +628,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public RepositoryInvokerFactory repositoryInvokerFactory() {
return new UnwrappingRepositoryInvokerFactory(
new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService()));
new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService()), lookups);
}
@Bean
@@ -708,6 +713,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return new MappingAuditableBeanWrapperFactory(persistentEntities());
}
@Bean
public SelfLinkProvider selfLinkProvider() {
return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), lookups);
}
protected List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
@@ -715,7 +725,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
projectionFactory.setResourceLoader(applicationContext);
PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver(
persistentEntities(), entityLinks(), config().getProjectionConfiguration(), projectionFactory,
persistentEntities(), selfLinkProvider(), config().getProjectionConfiguration(), projectionFactory,
resourceMappings());
HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver();

View File

@@ -26,7 +26,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
@@ -36,10 +35,10 @@ 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.ResourceMappings;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
import org.springframework.data.rest.webmvc.mapping.LinkCollectingAssociationHandler;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.Resource;
@@ -91,15 +90,16 @@ public class PersistentEntityJackson2Module extends SimpleModule {
/**
* Creates a new {@link PersistentEntityJackson2Module} using the given {@link ResourceMappings}, {@link Repositories}
* , {@link RepositoryRestConfiguration} and {@link UriToEntityConverter}.
* , {@link RepositoryRestConfiguration}, {@link UriToEntityConverter} and {@link SelfLinkProvider}.
*
* @param mappings must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param config must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param linkProvider must not be {@literal null}.
*/
public PersistentEntityJackson2Module(ResourceMappings mappings, PersistentEntities entities,
RepositoryRestConfiguration config, UriToEntityConverter converter, EntityLinks entityLinks) {
RepositoryRestConfiguration config, UriToEntityConverter converter, SelfLinkProvider linkProvider) {
super(new Version(2, 0, 0, null, "org.springframework.data.rest", "jackson-module"));
@@ -107,9 +107,10 @@ public class PersistentEntityJackson2Module extends SimpleModule {
Assert.notNull(entities, "Repositories must not be null!");
Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
Assert.notNull(linkProvider, "SelfLinkProvider must not be null!");
AssociationLinks associationLinks = new AssociationLinks(mappings);
LinkCollector collector = new LinkCollector(entities, entityLinks, associationLinks);
LinkCollector collector = new LinkCollector(entities, linkProvider, associationLinks);
addSerializer(new PersistentEntityResourceSerializer(collector));
addSerializer(new ProjectionSerializer(collector, mappings));
@@ -344,8 +345,8 @@ public class PersistentEntityJackson2Module extends SimpleModule {
if (persistentProperty.isCollectionLike()) {
CollectionLikeType collectionType = config.getTypeFactory().constructCollectionLikeType(
persistentProperty.getType(), persistentProperty.getActualType());
CollectionLikeType collectionType = config.getTypeFactory()
.constructCollectionLikeType(persistentProperty.getType(), persistentProperty.getActualType());
CollectionValueInstantiator instantiator = new CollectionValueInstantiator(persistentProperty);
CollectionDeserializer collectionDeserializer = new CollectionDeserializer(collectionType,
uriStringDeserializer, null, instantiator);
@@ -454,8 +455,8 @@ public class PersistentEntityJackson2Module extends SimpleModule {
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(TargetAware value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
public void serialize(TargetAware value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
Object target = value.getTarget();
Links links = mappings.getMetadataFor(value.getTargetClass()).isExported() ? collector.getLinksFor(target)
@@ -577,22 +578,23 @@ public class PersistentEntityJackson2Module extends SimpleModule {
private final PersistentEntities entities;
private final AssociationLinks associationLinks;
private final EntityLinks links;
private final SelfLinkProvider links;
/**
* Creates a new {@link PersistentEntities}, {@link EntityLinks} and {@link AssociationLinks}.
* Creates a new {@link PersistentEntities}, {@link SelfLinkProvider} and {@link AssociationLinks}.
*
* @param entities must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param linkProvider must not be {@literal null}.
* @param associationLinks must not be {@literal null}.
*/
public LinkCollector(PersistentEntities entities, EntityLinks entityLinks, AssociationLinks associationLinks) {
public LinkCollector(PersistentEntities entities, SelfLinkProvider linkProvider,
AssociationLinks associationLinks) {
Assert.notNull(entities, "PersistentEntities must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(linkProvider, "SelfLinkProvider must not be null!");
Assert.notNull(associationLinks, "AssociationLinks must not be null!");
this.links = entityLinks;
this.links = linkProvider;
this.entities = entities;
this.associationLinks = associationLinks;
}
@@ -622,7 +624,7 @@ public class PersistentEntityJackson2Module extends SimpleModule {
PersistentEntity<?, ?> entity = entities.getPersistentEntity(object.getClass());
Links links = new Links(existingLinks);
Link selfLink = createSelfLink(object, entity, links);
Link selfLink = createSelfLink(object, links);
if (selfLink == null) {
return links;
@@ -636,10 +638,10 @@ public class PersistentEntityJackson2Module extends SimpleModule {
List<Link> result = new ArrayList<Link>(existingLinks);
result.addAll(handler.getLinks());
return addSelfLinkIfNecessary(object, entity, result);
return addSelfLinkIfNecessary(object, result);
}
private Links addSelfLinkIfNecessary(Object object, PersistentEntity<?, ?> entity, List<Link> existing) {
private Links addSelfLinkIfNecessary(Object object, List<Link> existing) {
Links result = new Links(existing);
@@ -648,26 +650,19 @@ public class PersistentEntityJackson2Module extends SimpleModule {
}
List<Link> list = new ArrayList<Link>();
list.add(createSelfLink(object, entity, result));
list.add(createSelfLink(object, result));
list.addAll(existing);
return new Links(list);
}
private Link createSelfLink(Object object, PersistentEntity<?, ?> entity, Links existing) {
private Link createSelfLink(Object object, Links existing) {
if (existing.hasLink(Link.REL_SELF)) {
return existing.getLink(Link.REL_SELF);
}
IdentifierAccessor accessor = entity.getIdentifierAccessor(object);
Object identifier = accessor.getIdentifier();
if (identifier == null) {
return null;
}
return links.linkToSingleResource(entity.getType(), identifier).withSelfRel();
return links.createSelfLinkFor(object).withSelfRel();
}
}
@@ -712,8 +707,8 @@ public class PersistentEntityJackson2Module extends SimpleModule {
Class<?> collectionOrMapType = property.getType();
return property.isMap() ? CollectionFactory.createMap(collectionOrMapType, 0) : CollectionFactory
.createCollection(collectionOrMapType, 0);
return property.isMap() ? CollectionFactory.createMap(collectionOrMapType, 0)
: CollectionFactory.createCollection(collectionOrMapType, 0);
}
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.rest.webmvc;
import java.util.Collections;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +28,9 @@ import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -53,7 +58,11 @@ public abstract class AbstractControllerIntegrationTests {
@Bean
public PersistentEntityResourceAssembler persistentEntityResourceAssembler() {
return new PersistentEntityResourceAssembler(persistentEntities(), entityLinks(), StubProjector.INSTANCE,
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks(),
Collections.<EntityLookup<?>> emptyList());
return new PersistentEntityResourceAssembler(persistentEntities(), selfLinkProvider, StubProjector.INSTANCE,
resourceMappings());
}
}

View File

@@ -21,12 +21,15 @@ import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.math.BigInteger;
import java.util.Collections;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.mockito.internal.stubbing.answers.ReturnsArgumentAt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests.TestConfiguration;
import org.springframework.data.rest.webmvc.mongodb.MongoDbRepositoryConfig;
import org.springframework.data.rest.webmvc.mongodb.User;
@@ -57,8 +60,9 @@ public class PersistentEntityResourceAssemblerIntegrationTests extends AbstractC
when(projector.projectExcerpt(anyObject())).thenAnswer(new ReturnsArgumentAt(0));
PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities, entityLinks,
projector, mappings);
PersistentEntityResourceAssembler assembler = new PersistentEntityResourceAssembler(entities,
new DefaultSelfLinkProvider(entities, entityLinks, Collections.<EntityLookup<?>> emptyList()), projector,
mappings);
User user = new User();
user.id = BigInteger.valueOf(4711);
@@ -69,5 +73,4 @@ public class PersistentEntityResourceAssemblerIntegrationTests extends AbstractC
assertThat(links.getLink("self").getVariables(), is(Matchers.empty()));
assertThat(links.getLink("user").getVariableNames(), is(hasItem("projection")));
}
}

View File

@@ -269,7 +269,6 @@ public class PersistentEntitySerializationTests {
PersistentEntityResource resource = PersistentEntityResource.//
build(oliver, repositories.getPersistentEntity(Person.class)).//
withLink(new Link("/people/1")).//
withEmbedded(Arrays.asList(wrapper)).//
build();

View File

@@ -36,6 +36,9 @@ import org.springframework.data.rest.core.config.MetadataConfiguration;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
import org.springframework.data.rest.core.support.DefaultSelfLinkProvider;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.data.rest.webmvc.jpa.Person;
import org.springframework.data.rest.webmvc.jpa.PersonRepository;
@@ -113,9 +116,11 @@ public class RepositoryTestsConfig {
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
mock(PagingAndSortingTemplateVariables.class),
OrderAwarePluginRegistry.<Class<?>, BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE)));
SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks,
Collections.<EntityLookup<?>> emptyList());
return new PersistentEntityJackson2Module(mappings, persistentEntities(), config(),
new UriToEntityConverter(persistentEntities(), defaultConversionService()), entityLinks);
new UriToEntityConverter(persistentEntities(), defaultConversionService()), selfLinkProvider);
}
@Bean

View File

@@ -3,6 +3,43 @@
There are many options to tailor Spring Data REST. These subsections show how.
== Customizing item resource URIs
By default the URI for item resources are comprised of the path segment used for the collection resource with the database identifier appended.
That allows us to use the repository's `findOne(…)` method to lookup entity instances.
As of Spring Data REST 2.5 this can be customized by registering an implementation of `EntityLookup` as Spring bean in your application.
Spring Data REST will pick those up and tweak the URI generation according to their implementation.
Assume a `User` with a `username` property that uniquely identifies it.
Also, assume we have a method `Optional<User> findByUsername(String username)` on the according repository.
This would allow us to implement a `UserEntityLookup` looking like this:
[source, java]
----
@Component
public class UserEntityLookup extends EntityLookupSupport<User> {
private final UserRepository repository;
public UserEntityLookup(UserRepository repository) {
this.repository = repository;
}
@Override
public Serializable getResourceIdentifier(User entity) {
return entity.getUsername();
}
@Override
public Object lookupEntity(Serializable id) {
return repository.findByUsername(id.toString());
}
}
----
Note, how `getResourceIdentifier(…)` returns the username to be used by the URI creation. To load entity instances by the value returned from that method we now implement `lookupEntity(…)` using the query method available on the `UserRepository`.
include::configuring-the-rest-url-path.adoc[leveloffset=+1]
include::adding-sdr-to-spring-mvc-app.adoc[leveloffset=+1]
include::overriding-sdr-response-handlers.adoc[leveloffset=+1]