diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml
index ccc5a6f5..d8bbb48c 100644
--- a/spring-cloud-commons/pom.xml
+++ b/spring-cloud-commons/pom.xml
@@ -48,6 +48,16 @@
spring-integration-jmx
true
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+ true
+
+
+ com.jayway.jsonpath
+ json-path
+ true
+
org.projectlombok
lombok
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfiguration.java
new file mode 100644
index 00000000..a30f4f0c
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import lombok.Data;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration.CloudHypermediaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Registers a default {@link RemoteResourceRefresher} if at least one {@link RemoteResource} is declared in the system
+ * and applies verification timings defined in the application properties.
+ *
+ * @author Oliver Gierke
+ */
+@Configuration
+@ConditionalOnBean(type = "org.springframework.cloud.client.hypermedia.RemoteResource")
+@EnableConfigurationProperties(CloudHypermediaProperties.class)
+public class CloudHypermediaAutoConfiguration {
+
+ @Autowired(required = false) List discoveredResources = Collections.emptyList();
+ @Autowired CloudHypermediaProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RemoteResourceRefresher discoveredResourceRefresher() {
+ return new RemoteResourceRefresher(discoveredResources, properties.getRefresh().getFixedDelay(),
+ properties.getRefresh().getInitialDelay());
+ }
+
+ @Data
+ @ConfigurationProperties(prefix = "spring.cloud.hypermedia")
+ public static class CloudHypermediaProperties {
+
+ private Refresh refresh = new Refresh();
+
+ @Data
+ public static class Refresh {
+
+ private int fixedDelay = 5000;
+ private int initialDelay = 10000;
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DiscoveredResource.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DiscoveredResource.java
new file mode 100644
index 00000000..6a9737a1
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DiscoveredResource.java
@@ -0,0 +1,121 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.net.URI;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * A REST resource that is defined by a service reference and a traversal operation within that service.
+ *
+ * @author Oliver Gierke
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class DiscoveredResource implements RemoteResource {
+
+ private final ServiceInstanceProvider provider;
+ private final TraversalDefinition traversal;
+
+ private RestOperations restOperations = new RestTemplate();
+ private @Getter Link link = null;
+
+ /**
+ * Configures the {@link RestOperations} to use to execute the traversal and verifying HEAD calls.
+ *
+ * @param restOperations can be {@literal null}, resorting to a default {@link RestTemplate} in that case.
+ */
+ public void setRestOperations(RestOperations restOperations) {
+ this.restOperations = restOperations == null ? new RestTemplate() : restOperations;
+ }
+
+ /**
+ * Verifies the link to the current
+ */
+ public void verifyOrDiscover() {
+ this.link = link == null ? discoverLink() : verify(link);
+ }
+
+ /**
+ * Verifies the given {@link Link} by issuing an HTTP HEAD request to the resource.
+ *
+ * @param link must not be {@literal null}.
+ * @return
+ */
+ private Link verify(Link link) {
+
+ Assert.notNull(link, "Link must not be null!");
+
+ try {
+
+ String uri = link.expand().getHref();
+
+ log.debug("Verifying link pointing to {}…", uri);
+ restOperations.headForHeaders(uri);
+ log.debug("Successfully verified link!");
+
+ return link;
+
+ } catch (RestClientException o_O) {
+
+ log.debug("Verification failed, marking as outdated!");
+ return null;
+ }
+ }
+
+ private Link discoverLink() {
+
+ try {
+
+ ServiceInstance service = provider.getServiceInstance();
+
+ if (service == null) {
+ return null;
+ }
+
+ URI uri = service.getUri();
+ String serviceId = service.getServiceId();
+
+ log.debug("Discovered {} system at {}. Discovering resource…", serviceId, uri);
+
+ Traverson traverson = new Traverson(uri, MediaTypes.HAL_JSON);
+ Link link = traversal.buildTraversal(traverson).asTemplatedLink();
+
+ log.debug("Found link pointing to {}.", link.getHref());
+
+ return link;
+
+ } catch (RuntimeException o_O) {
+
+ this.link = null;
+ log.debug("Target system unavailable. Got: ", o_O.getMessage());
+
+ return null;
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProvider.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProvider.java
new file mode 100644
index 00000000..7f33015a
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+
+/**
+ * {@link ServiceInstanceProvider} to work with a {@link DiscoveryClient} to lookup a service by name. Will pick the
+ * first one returned by the configured {@link DiscoveryClient}.
+ *
+ * @author Oliver Gierke
+ */
+@RequiredArgsConstructor
+public class DynamicServiceInstanceProvider implements ServiceInstanceProvider {
+
+ private final DiscoveryClient client;
+ private final String serviceName;
+
+ /*
+ * (non-Javadoc)
+ * @see example.customers.integration.ServiceInstanceProvider#getServiceInstance()
+ */
+ @Override
+ public ServiceInstance getServiceInstance() {
+
+ try {
+
+ List instances = client.getInstances(serviceName);
+ return instances.isEmpty() ? null : instances.get(0);
+
+ } catch (RuntimeException o_O) {
+ return null;
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResource.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResource.java
new file mode 100644
index 00000000..03da9442
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResource.java
@@ -0,0 +1,40 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import org.springframework.hateoas.Link;
+
+/**
+ * A REST resource that can be discovered and can be either gone or available.
+ *
+ * @author Oliver Gierke
+ */
+public interface RemoteResource {
+
+ /**
+ * Returns the {@link Link} to the resource in case it is available or {@literal null} in case it's gone, i.e. either
+ * generally unavailable or can't be discovered.
+ *
+ * @return
+ */
+ Link getLink();
+
+ /**
+ * Discovers the the resource in case it hasn't been yet or became unavailable. In case a link has been discovered
+ * previously, it is verified and either confirmed or the link is removed to indicate it's not available anymore.
+ */
+ void verifyOrDiscover();
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResourceRefresher.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResourceRefresher.java
new file mode 100644
index 00000000..d3effdad
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResourceRefresher.java
@@ -0,0 +1,57 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+import org.springframework.scheduling.config.ContextLifecycleScheduledTaskRegistrar;
+import org.springframework.scheduling.config.IntervalTask;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+
+/**
+ * A {@link ScheduledTaskRegistrar} that verifies all {@link DiscoveredResource} instances in the system based
+ * on the given timing configuration.
+ *
+ * @author Oliver Gierke
+ */
+@RequiredArgsConstructor
+public class RemoteResourceRefresher extends ContextLifecycleScheduledTaskRegistrar {
+
+ private final List discoveredResources;
+ private final int fixedDelay, initialDelay;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.scheduling.config.ContextLifecycleScheduledTaskRegistrar#afterPropertiesSet()
+ */
+ @Override
+ public void afterPropertiesSet() {
+
+ for (final RemoteResource resource : discoveredResources) {
+ addFixedDelayTask(new IntervalTask(new Runnable() {
+
+ @Override
+ public void run() {
+ resource.verifyOrDiscover();
+ }
+ }, fixedDelay, initialDelay));
+ }
+
+ super.afterPropertiesSet();
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/ServiceInstanceProvider.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/ServiceInstanceProvider.java
new file mode 100644
index 00000000..f7547baf
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/ServiceInstanceProvider.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * A component that will provide a {@link ServiceInstance} or can express the absence of one by returning
+ * {@literal null}.
+ *
+ * @author Oliver Gierke
+ */
+public interface ServiceInstanceProvider {
+
+ /**
+ * Returns the service instance or {@literal null} in case the service is currently unavailable.
+ *
+ * @return the service instance or {@literal null} in case the service is currently unavailable.
+ */
+ ServiceInstance getServiceInstance();
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/StaticServiceInstanceProvider.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/StaticServiceInstanceProvider.java
new file mode 100644
index 00000000..dfebb846
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/StaticServiceInstanceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+/**
+ * A {@link ServiceInstanceProvider} that will always return the configured {@link ServiceInstance}.
+ *
+ * @author Oliver Gierke
+ */
+@RequiredArgsConstructor
+public class StaticServiceInstanceProvider implements ServiceInstanceProvider {
+
+ private final ServiceInstance instance;
+
+ /*
+ * (non-Javadoc)
+ * @see example.customers.integration.ServiceInstanceProvider#getServiceInstance()
+ */
+ @Override
+ public ServiceInstance getServiceInstance() {
+ return instance;
+ }
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/TraversalDefinition.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/TraversalDefinition.java
new file mode 100644
index 00000000..88bb8fc4
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/TraversalDefinition.java
@@ -0,0 +1,33 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.hateoas.client.Traverson.TraversalBuilder;
+
+/**
+ * Callback to define the traversal to a resource.
+ *
+ * @author Oliver Gierke
+ */
+public interface TraversalDefinition {
+
+ /**
+ * @param traverson the Traverson instance to run the traversal on.
+ * @return
+ */
+ TraversalBuilder buildTraversal(Traverson traverson);
+}
diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories
index df47a829..da8a7d3f 100644
--- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories
@@ -2,6 +2,7 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\
+org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\
org.springframework.cloud.util.UtilAutoConfiguration
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfigurationIntegrationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfigurationIntegrationTests.java
new file mode 100644
index 00000000..90c59b31
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfigurationIntegrationTests.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2016 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.cloud.client.hypermedia;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration.CloudHypermediaProperties;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.hateoas.client.Traverson.TraversalBuilder;
+
+/**
+ * Integration tests for {@link CloudHypermediaAutoConfiguration}.
+ *
+ * @author Oliver Gierke
+ */
+public class CloudHypermediaAutoConfigurationIntegrationTests {
+
+ @Test
+ public void picksUpHypermediaProperties() {
+
+ try (ConfigurableApplicationContext context = getApplicationContext(ConfigWithRemoteResource.class)) {
+
+ CloudHypermediaProperties properties = context.getBean(CloudHypermediaProperties.class);
+
+ assertThat(properties.getRefresh().getInitialDelay(), is(50000));
+ assertThat(properties.getRefresh().getFixedDelay(), is(10000));
+ }
+ }
+
+ @Test
+ public void doesNotCreateCloudHypermediaPropertiesifNotActive() {
+
+ try (ConfigurableApplicationContext context = getApplicationContext(Config.class)) {
+ assertThat(context.getBeanNamesForType(CloudHypermediaProperties.class), is(arrayWithSize(0)));
+ }
+ }
+
+ @Test
+ public void doesNotRegisterResourceRefresherIfNoDiscoveredResourceIsDefined() {
+
+ try (ConfigurableApplicationContext context = getApplicationContext(Config.class)) {
+
+ assertThat(context.getBeansOfType(RemoteResource.class).values(), hasSize(0));
+ assertThat(context.getBeanNamesForType(RemoteResourceRefresher.class), is(arrayWithSize(0)));
+ }
+ }
+
+ @Test
+ public void registersResourceRefresherIfDiscoverredResourceIsDefined() {
+
+ try (ConfigurableApplicationContext context = getApplicationContext(ConfigWithRemoteResource.class)) {
+
+ assertThat(context.getBeansOfType(RemoteResource.class).values(), hasSize(1));
+ assertThat(context.getBean(RemoteResourceRefresher.class), is(notNullValue()));
+ }
+ }
+
+ private static ConfigurableApplicationContext getApplicationContext(Class> configuration) {
+ return SpringApplication.run(configuration, new String[0]);
+ }
+
+ @Configuration
+ @EnableAutoConfiguration
+ static class Config {}
+
+ @Configuration
+ @EnableAutoConfiguration
+ static class ConfigWithRemoteResource {
+
+ @Bean
+ public RemoteResource resource() {
+
+ ServiceInstanceProvider provider = new StaticServiceInstanceProvider(
+ new DefaultServiceInstance("service", "localhost", 80, false));
+ return new DiscoveredResource(provider, new TraversalDefinition() {
+
+ @Override
+ public TraversalBuilder buildTraversal(Traverson traverson) {
+ return traverson.follow("rel");
+ }
+ });
+ }
+ }
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DiscoveredResourceUnitTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DiscoveredResourceUnitTests.java
new file mode 100644
index 00000000..599322d7
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DiscoveredResourceUnitTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.hateoas.client.Traverson.TraversalBuilder;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestOperations;
+
+/**
+ * @author Oliver Gierke
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DiscoveredResourceUnitTests {
+
+ @Mock ServiceInstanceProvider provider;
+ @Mock TraversalDefinition traversal;
+ @Mock TraversalBuilder builder;
+ @Mock RestOperations operations;
+
+ DiscoveredResource resource;
+
+ @Before
+ public void setUp() {
+ when(traversal.buildTraversal(Matchers.any(Traverson.class))).thenReturn(builder);
+
+ this.resource = new DiscoveredResource(provider, traversal);
+ this.resource.setRestOperations(operations);
+ }
+
+ @Test
+ public void isUndiscoveredByDefault() {
+ assertThat(resource.getLink(), is(nullValue()));
+ }
+
+ @Test
+ public void verificationTriggersDiscovery() {
+
+ Link link = new Link("target", "rel");
+
+ when(provider.getServiceInstance()).thenReturn(new DefaultServiceInstance("service", "localhost", 8080, false));
+ when(builder.asTemplatedLink()).thenReturn(link);
+
+ resource.verifyOrDiscover();
+
+ assertThat(resource.getLink(), is(link));
+ verify(provider, times(1)).getServiceInstance();
+ verify(traversal, times(1)).buildTraversal(Matchers.any(Traverson.class));
+ }
+
+ @Test
+ public void triggersVerificationOnSubsequentCall() {
+
+ verificationTriggersDiscovery();
+
+ resource.verifyOrDiscover();
+
+ assertThat(resource.getLink(), is(notNullValue()));
+ verify(operations, times(1)).headForHeaders(anyString());
+ }
+
+ @Test
+ public void resetsLinkOnFailedVerification() {
+
+ verificationTriggersDiscovery();
+
+ doThrow(RestClientException.class).when(operations).headForHeaders(anyString());
+ resource.verifyOrDiscover();
+
+ assertThat(resource.getLink(), is(nullValue()));
+ }
+
+ @Test
+ public void failedDiscoveryTraversalCausesLinkToStayNull() {
+
+ doThrow(RuntimeException.class).when(provider).getServiceInstance();
+
+ resource.verifyOrDiscover();
+
+ assertThat(resource.getLink(), is(nullValue()));
+ }
+}
diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProviderUnitTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProviderUnitTests.java
new file mode 100644
index 00000000..a7ac5bd0
--- /dev/null
+++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProviderUnitTests.java
@@ -0,0 +1,55 @@
+/*
+ * 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.cloud.client.hypermedia;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+
+/**
+ * @author Oliver Gierke
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DynamicServiceInstanceProviderUnitTests {
+
+ @Mock DiscoveryClient client;
+
+ @Test
+ public void returnsNoServiceInCaseNoneIsAvailable() {
+ assertThat(new DynamicServiceInstanceProvider(client, "service").getServiceInstance(), is(nullValue()));
+ }
+
+ @Test
+ public void returnsFirstServiceInCaseMultipleOnesAreAvailable() {
+
+ ServiceInstance first = mock(ServiceInstance.class);
+ ServiceInstance second = mock(ServiceInstance.class);
+
+ when(client.getInstances(anyString())).thenReturn(Arrays.asList(first, second));
+
+ assertThat(new DynamicServiceInstanceProvider(client, "service").getServiceInstance(), is(first));
+ }
+}
diff --git a/spring-cloud-commons/src/test/resources/application.properties b/spring-cloud-commons/src/test/resources/application.properties
index 961e7d22..65d9c308 100644
--- a/spring-cloud-commons/src/test/resources/application.properties
+++ b/spring-cloud-commons/src/test/resources/application.properties
@@ -2,4 +2,7 @@ message: Hello scope!
delay: 0
debug: true
#logging.level.org.springframework.web: DEBUG
-#logging.level.org.springframework.context.annotation: DEBUG
\ No newline at end of file
+#logging.level.org.springframework.context.annotation: DEBUG
+
+spring.cloud.hypermedia.refresh.initial-delay=50000
+spring.cloud.hypermedia.refresh.fixed-delay=10000