diff --git a/spring-data-rest-core/pom.xml b/spring-data-rest-core/pom.xml
index 981b65a0a..3c1933bab 100644
--- a/spring-data-rest-core/pom.xml
+++ b/spring-data-rest-core/pom.xml
@@ -65,6 +65,13 @@
${jackson}
+
+ com.google.guava
+ guava
+ ${guava}
+ true
+
+
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
new file mode 100644
index 000000000..a17554abc
--- /dev/null
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactory.java
@@ -0,0 +1,243 @@
+/*
+ * 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 java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.core.convert.converter.Converter;
+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.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * {@link RepositoryInvokerFactory} that wraps the {@link RepositoryInvokerFactory} returned by the delegate with one
+ * that automatically unwraps JDK 8 {@link Optional} and Guava {@link com.google.common.base.Optional}s.
+ *
+ * @author Oliver Gierke
+ */
+public class UnwrappingRepositoryInvokerFactory implements RepositoryInvokerFactory {
+
+ private static final List> CONVERTERS;
+
+ static {
+
+ List> converters = new ArrayList>();
+ ClassLoader classLoader = UnwrappingRepositoryInvokerFactory.class.getClassLoader();
+
+ // Add unwrapper for Java 8 Optional
+
+ if (ClassUtils.isPresent("java.util.Optional", classLoader)) {
+ converters.add(new Converter() {
+ @Override
+ public Object convert(Object source) {
+ return source instanceof Optional ? ((Optional>) source).orElse(null) : source;
+ }
+ });
+ }
+
+ // Add unwrapper for Guava Optional
+
+ if (ClassUtils.isPresent("com.google.common.base.Optional", classLoader)) {
+
+ 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;
+ }
+ });
+ }
+
+ CONVERTERS = Collections.unmodifiableList(converters);
+ }
+
+ private final RepositoryInvokerFactory delegate;
+
+ /**
+ * Creates a new {@link UnwrappingRepositoryInvokerFactory}.
+ *
+ * @param delegate must not be {@literal null}.
+ */
+ public UnwrappingRepositoryInvokerFactory(RepositoryInvokerFactory delegate) {
+
+ Assert.notNull(delegate, "Delegate RepositoryInvokerFactory must not be null!");
+
+ this.delegate = delegate;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvokerFactory#getInvokerFor(java.lang.Class)
+ */
+ @Override
+ public RepositoryInvoker getInvokerFor(Class> domainType) {
+ return new UnwrappingRepositoryInvoker(delegate.getInvokerFor(domainType), CONVERTERS);
+ }
+
+ /**
+ * {@link RepositoryInvoker} that post-processes invocations of {@link RepositoryInvoker#invokeFindOne(Serializable)}
+ * and {@link #invokeQueryMethod(Method, MultiValueMap, Pageable, Sort)} using the given {@link Converter}s.
+ *
+ * @author Oliver Gierke
+ */
+ 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;
+ }
+
+ /*
+ * (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));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeQueryMethod(java.lang.reflect.Method, java.util.Map, org.springframework.data.domain.Pageable, org.springframework.data.domain.Sort)
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public Object invokeQueryMethod(Method method, Map parameters, Pageable pageable, Sort sort) {
+ return postProcess(delegate.invokeQueryMethod(method, parameters, pageable, sort));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeQueryMethod(java.lang.reflect.Method, org.springframework.util.MultiValueMap, org.springframework.data.domain.Pageable, org.springframework.data.domain.Sort)
+ */
+ @Override
+ public Object invokeQueryMethod(Method method, MultiValueMap parameters,
+ Pageable pageable, Sort sort) {
+ return postProcess(delegate.invokeQueryMethod(method, parameters, pageable, sort));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvocationInformation#hasDeleteMethod()
+ */
+ @Override
+ public boolean hasDeleteMethod() {
+ return delegate.hasDeleteMethod();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvocationInformation#hasFindAllMethod()
+ */
+ @Override
+ public boolean hasFindAllMethod() {
+ return delegate.hasFindAllMethod();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvocationInformation#hasFindOneMethod()
+ */
+ @Override
+ public boolean hasFindOneMethod() {
+ return delegate.hasFindOneMethod();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvocationInformation#hasSaveMethod()
+ */
+ @Override
+ public boolean hasSaveMethod() {
+ return delegate.hasSaveMethod();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeDelete(java.io.Serializable)
+ */
+ @Override
+ public void invokeDelete(Serializable id) {
+ delegate.invokeDelete(id);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable)
+ */
+ @Override
+ public Iterable invokeFindAll(Pageable pageable) {
+ return delegate.invokeFindAll(pageable);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort)
+ */
+ @Override
+ public Iterable invokeFindAll(Sort sort) {
+ return delegate.invokeFindAll(sort);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.support.RepositoryInvoker#invokeSave(java.lang.Object)
+ */
+ @Override
+ public T invokeSave(T object) {
+ return delegate.invokeSave(object);
+ }
+
+ /**
+ * Invokes the configured converters for the given result.
+ *
+ * @param result can be {@literal null}.
+ * @return
+ */
+ private Object postProcess(Object result) {
+
+ for (Converter converter : converters) {
+ result = converter.convert(result);
+ }
+
+ return result;
+ }
+ }
+}
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
new file mode 100644
index 000000000..b0a98d343
--- /dev/null
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/support/UnwrappingRepositoryInvokerFactoryUnitTests.java
@@ -0,0 +1,105 @@
+/*
+ * 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.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+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.util.LinkedMultiValueMap;
+
+/**
+ * Unit tests for {@link UnwrappingRepositoryInvokerFactory}.
+ *
+ * @author Oliver Gierke
+ */
+@RunWith(Parameterized.class)
+public class UnwrappingRepositoryInvokerFactoryUnitTests {
+
+ static final Object REFERENCE = new Object();
+
+ RepositoryInvokerFactory delegate = mock(RepositoryInvokerFactory.class);
+ RepositoryInvoker invoker = mock(RepositoryInvoker.class);
+
+ RepositoryInvokerFactory factory;
+ Method method;
+
+ public @Parameter(value = 0) Object source;
+ public @Parameter(value = 1) Matcher value;
+
+ @Before
+ public void setUp() throws Exception {
+
+ when(delegate.getInvokerFor(Object.class)).thenReturn(invoker);
+
+ this.factory = new UnwrappingRepositoryInvokerFactory(delegate);
+ this.method = Object.class.getMethod("toString");
+ }
+
+ @Parameters
+ 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) } //
+ });
+ }
+
+ /**
+ * @see DATAREST-511
+ */
+ @Test
+ public void unwrapsValuesForFindOne() {
+ assertFindOneValueForSource(source, value);
+ }
+
+ /**
+ * @see DATAREST-511
+ */
+ @Test
+ public void unwrapsValuesForQuery() {
+ assertQueryValueForSource(source, value);
+ }
+
+ private void assertFindOneValueForSource(Object source, Matcher value) {
+
+ when(invoker.invokeFindOne(1L)).thenReturn(source);
+ assertThat(factory.getInvokerFor(Object.class).invokeFindOne(1L), value);
+ }
+
+ 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);
+ }
+}
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 210ad7a7f..0132f9feb 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
@@ -65,6 +65,7 @@ import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.support.RepositoryRelProvider;
+import org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping;
import org.springframework.data.rest.webmvc.BaseUri;
@@ -575,7 +576,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public RepositoryInvokerFactory repositoryInvokerFactory() {
- return new DefaultRepositoryInvokerFactory(repositories(), defaultConversionService());
+ return new UnwrappingRepositoryInvokerFactory(new DefaultRepositoryInvokerFactory(repositories(),
+ defaultConversionService()));
}
@Bean
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/TestMvcClient.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/TestMvcClient.java
index abd8372df..2e7e0d0b5 100644
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/TestMvcClient.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/TestMvcClient.java
@@ -20,6 +20,8 @@ import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import java.util.Arrays;
+import java.util.Iterator;
import java.util.List;
import org.springframework.hateoas.Link;
@@ -184,6 +186,27 @@ public class TestMvcClient {
return discover.get(0);
}
+ /**
+ * Traverses the given link relations from the root.
+ *
+ * @param rels
+ * @return
+ * @throws Exception
+ */
+ public Link discoverUnique(String... rels) throws Exception {
+
+ Iterator toTraverse = Arrays.asList(rels).iterator();
+ Link lastLink = null;
+
+ while (toTraverse.hasNext()) {
+
+ String rel = toTraverse.next();
+ lastLink = lastLink == null ? discoverUnique(rel) : discoverUnique(lastLink, rel);
+ }
+
+ return lastLink;
+ }
+
/**
* Given a URI (root), discover the URIs for a given rel.
*
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
index e4a2906ca..2e5b32017 100644
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
@@ -301,4 +301,18 @@ public class MongoWebTests extends CommonWebTests {
andExpect(status().isNotModified()).//
andExpect(header().string(ETAG, is(notNullValue())));
}
+
+ /**
+ * @see DATAREST-511
+ */
+ @Test
+ public void invokesQueryResourceReturningAnOptional() throws Exception {
+
+ Profile profile = repository.findAll().iterator().next();
+
+ Link link = client.discoverUnique("profiles", "search", "findById");
+
+ mvc.perform(get(link.expand(profile.getId()).getHref())).//
+ andExpect(status().isOk());
+ }
}
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/ProfileRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/ProfileRepository.java
index 10552c059..bedd42ed5 100644
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/ProfileRepository.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/ProfileRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2014 the original author or authors.
+ * Copyright 2012-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.
@@ -16,6 +16,7 @@
package org.springframework.data.rest.webmvc.mongodb;
import java.util.List;
+import java.util.Optional;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
@@ -32,4 +33,9 @@ public interface ProfileRepository extends PagingAndSortingRepository findById(@Param("id") String id);
}