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

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

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

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.core.support;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.rest.core.domain.mongodb.Profile;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
/**
* Unit tests for {@link DefaultSelfLinkProvider}.
*
* @author Oliver Gierke
* @soundtrack Trio Rotation - Triopane
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultSelfLinkProviderUnitTests {
SelfLinkProvider provider;
@Mock EntityLinks entityLinks;
PersistentEntities entities;
List<EntityLookup<?>> lookups;
public @Rule ExpectedException exception = ExpectedException.none();
@Before
public void setUp() {
when(entityLinks.linkToSingleResource((Class<?>) any(), any())).then(new Answer<Link>() {
@Override
public Link answer(InvocationOnMock invocation) throws Throwable {
Class<?> type = invocation.getArgumentAt(0, Class.class);
Serializable id = invocation.getArgumentAt(1, Serializable.class);
return new Link("/".concat(type.getName()).concat("/").concat(id.toString()));
}
});
MongoMappingContext context = new MongoMappingContext();
context.getPersistentEntity(Profile.class);
context.afterPropertiesSet();
this.entities = new PersistentEntities(Arrays.asList(context));
this.lookups = Collections.emptyList();
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntities() {
new DefaultSelfLinkProvider(null, entityLinks, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntityLinks() {
new DefaultSelfLinkProvider(entities, null, lookups);
}
/**
* @see DATAREST-724
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullEntityLookups() {
new DefaultSelfLinkProvider(entities, entityLinks, null);
}
/**
* @see DATAREST-724
*/
@Test
public void usesEntityIdIfNoLookupDefined() {
String id = UUID.randomUUID().toString();
Link link = provider.createSelfLinkFor(new Profile(id, "Name", "Type"));
assertThat(link.getHref(), Matchers.endsWith(id));
}
/**
* @see DATAREST-724
*/
@Test
@SuppressWarnings("unchecked")
public void usesEntityLookupIfDefined() {
EntityLookup<Object> lookup = mock(EntityLookup.class);
when(lookup.supports(Profile.class)).thenReturn(true);
when(lookup.getResourceIdentifier(any(Profile.class))).thenReturn("foo");
this.provider = new DefaultSelfLinkProvider(entities, entityLinks, Arrays.asList(lookup));
String id = UUID.randomUUID().toString();
Link link = provider.createSelfLinkFor(new Profile(id, "Name", "Type"));
assertThat(link.getHref(), Matchers.endsWith("foo"));
}
/**
* @see DATAREST-724
*/
@Test
public void rejectsLinkCreationForUnknownEntity() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(Object.class.getName());
exception.expectMessage("No persistent entity found!");
provider.createSelfLinkFor(new Object());
}
}

View File

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