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]