From 44ab7568732f3ee69b677e11cfb0f2a2589b1113 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 9 Dec 2015 11:41:05 +0100 Subject: [PATCH] DATAREST-724 - Added support for customizing the property to be used for URI generation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- lombok.config | 2 + .../core/support/DefaultSelfLinkProvider.java | 99 ++++++++++++ .../data/rest/core/support/EntityLookup.java | 58 +++++++ .../core/support/EntityLookupSupport.java | 47 ++++++ .../rest/core/support/SelfLinkProvider.java | 36 +++++ .../UnwrappingRepositoryInvokerFactory.java | 53 +++--- .../DefaultSelfLinkProviderUnitTests.java | 152 ++++++++++++++++++ ...pingRepositoryInvokerFactoryUnitTests.java | 36 ++++- .../PersistentEntityResourceAssembler.java | 35 ++-- ...tityResourceAssemblerArgumentResolver.java | 17 +- .../RepositoryRestMvcConfiguration.java | 16 +- .../json/PersistentEntityJackson2Module.java | 55 +++---- .../AbstractControllerIntegrationTests.java | 11 +- ...tityResourceAssemblerIntegrationTests.java | 9 +- .../PersistentEntitySerializationTests.java | 1 - .../webmvc/json/RepositoryTestsConfig.java | 7 +- src/main/asciidoc/customizing-sdr.adoc | 37 +++++ 17 files changed, 564 insertions(+), 107 deletions(-) create mode 100644 lombok.config create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookupSupport.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/SelfLinkProvider.java create mode 100644 spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java diff --git a/lombok.config b/lombok.config new file mode 100644 index 000000000..e50c7ea43 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.log.fieldName = LOG diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java new file mode 100644 index 000000000..50c17b2cf --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/DefaultSelfLinkProvider.java @@ -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, 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> 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 instanceType = instance.getClass(); + + EntityLookup lookup = (EntityLookup) 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(); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java new file mode 100644 index 000000000..bc3ecfcf2 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java @@ -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 extends Plugin> { + + /** + * 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)}. + *

+ * 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); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookupSupport.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookupSupport.java new file mode 100644 index 000000000..a1f8d75b4 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookupSupport.java @@ -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 implements EntityLookup { + + 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); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/SelfLinkProvider.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/SelfLinkProvider.java new file mode 100644 index 000000000..cef33ebe6 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/SelfLinkProvider.java @@ -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); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactory.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactory.java index a17554abc..dc313fba6 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactory.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactory.java @@ -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() { @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, 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> 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> 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> 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> converters; + private final EntityLookup lookup; /* * (non-Javadoc) * @see org.springframework.data.repository.support.RepositoryInvoker#invokeFindOne(java.io.Serializable) */ - @SuppressWarnings("unchecked") + public 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 postProcess(Object result) { for (Converter converter : converters) { result = converter.convert(result); } - return result; + return (T) result; } } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java new file mode 100644 index 000000000..c39c67117 --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/DefaultSelfLinkProviderUnitTests.java @@ -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> lookups; + + public @Rule ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() { + + when(entityLinks.linkToSingleResource((Class) any(), any())).then(new Answer() { + + @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 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()); + } +} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactoryUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactoryUnitTests.java index b0a98d343..0258be236 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactoryUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactoryUnitTests.java @@ -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.> emptyList()); this.method = Object.class.getMethod("toString"); } @@ -67,10 +70,10 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests { public static Collection data() { return Arrays.asList(new Object[][] { // { Optional.empty(), is(nullValue()) }, // - { Optional.of(REFERENCE), is(REFERENCE) }, // - { com.google.common.base.Optional.absent(), is(nullValue()) }, // - { com.google.common.base.Optional.of(REFERENCE), is(REFERENCE) } // - }); + { Optional.of(REFERENCE), is(REFERENCE) }, // + { com.google.common.base.Optional.absent(), is(nullValue()) }, // + { com.google.common.base.Optional.of(REFERENCE), is(REFERENCE) } // + }); } /** @@ -89,6 +92,24 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests { assertQueryValueForSource(source, value); } + /** + * @see DATAREST-724 + */ + @Test + @SuppressWarnings("unchecked") + public void usesRegisteredEntityLookup() { + + EntityLookup 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 value) { when(invoker.invokeFindOne(1L)).thenReturn(source); @@ -98,8 +119,7 @@ public class UnwrappingRepositoryInvokerFactoryUnitTests { private void assertQueryValueForSource(Object source, Matcher value) { when(invoker.invokeQueryMethod(method, new LinkedMultiValueMap(), null, null)).thenReturn(source); - assertThat( - factory.getInvokerFor(Object.class).invokeQueryMethod(method, new LinkedMultiValueMap(), null, - null), value); + assertThat(factory.getInvokerFor(Object.class).invokeQueryMethod(method, new LinkedMultiValueMap(), + null, null), value); } } 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 9a705a9c4..28c84185a 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 @@ -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 { 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 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); } } 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 index d5ef70b66..54a447080 100644 --- 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 @@ -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); } } 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 5e0adce8b..f63aef052 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 @@ -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 idConverters = Collections.emptyList(); @Autowired(required = false) List configurers = Collections.emptyList(); + @Autowired(required = false) List> 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 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(); 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 2832c2502..469f3d3ca 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 @@ -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 result = new ArrayList(existingLinks); result.addAll(handler.getLinks()); - return addSelfLinkIfNecessary(object, entity, result); + return addSelfLinkIfNecessary(object, result); } - private Links addSelfLinkIfNecessary(Object object, PersistentEntity entity, List existing) { + private Links addSelfLinkIfNecessary(Object object, List existing) { Links result = new Links(existing); @@ -648,26 +650,19 @@ public class PersistentEntityJackson2Module extends SimpleModule { } List list = new ArrayList(); - 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); } } } 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 82fa80f71..a7a163ef0 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 @@ -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.> emptyList()); + + return new PersistentEntityResourceAssembler(persistentEntities(), selfLinkProvider, StubProjector.INSTANCE, resourceMappings()); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java index 68a602274..215570171 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssemblerIntegrationTests.java @@ -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.> 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"))); } - } 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 f81695bb2..0d34e2659 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 @@ -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(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index 6840fa4c9..cda4a6138 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -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., BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE))); + SelfLinkProvider selfLinkProvider = new DefaultSelfLinkProvider(persistentEntities(), entityLinks, + Collections.> emptyList()); return new PersistentEntityJackson2Module(mappings, persistentEntities(), config(), - new UriToEntityConverter(persistentEntities(), defaultConversionService()), entityLinks); + new UriToEntityConverter(persistentEntities(), defaultConversionService()), selfLinkProvider); } @Bean diff --git a/src/main/asciidoc/customizing-sdr.adoc b/src/main/asciidoc/customizing-sdr.adoc index c9ca6d264..f6f345989 100644 --- a/src/main/asciidoc/customizing-sdr.adoc +++ b/src/main/asciidoc/customizing-sdr.adoc @@ -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 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 { + + 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]