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