From 526b338c3696b52912b2adfa4c749a74c69d44e0 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 23 Dec 2015 12:04:35 +0100 Subject: [PATCH] Added hypermedia support in the form of a RemoteResource abstraction. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces support to easily discover resources in remote service APIs and periodically validating they're still available. The core concept is a RemoteResource and its primary implementation DiscoveredResource which consists of a ServiceInstanceProvider and a TraversalDescription. The service instance provider abstracts the location of the service the DiscoveredResource is hosted at. Currently there are two implementations available: a StaticServiceInstanceProvider that — as the name suggests — returns a statically configured instance, as well as the DynamicServiceInstanceProvider which takes a DiscoveryClient and service name as input to dynamically discover the ServiceInstance on request. The TraversalDefinition is a SAM type that allows to define the path traversals that shall be executed to discover the target resource within the service instance. This allows the following configuration: DiscoveryClient client = … ServiceInstanceProvider provider = new DynamicServiceInstanceProvider(client, "stores"); RemoteResource resource = new DiscoveredResource(provider, traverson -> traverson.follow("stores", "search", "by-location"); This will cause the following to happen: 1. In this default state the resource is undiscovered and calls to getLink() will return null. 2. A call to verifyOrDiscover() will trigger resource discovery, i.e. lookup the service instance using the discovery client, execute the path traversal as defined and store the final link discovered ("by-location") in the discovered resource. 3. Calls to getLink() will return that link. Calls to verifyOrDiscover() will issue a HEAD request to verify the resource is still available and reset the link if not. Subsequent calls to verifyOrDiscover() will trigger rediscovery. That means that we basically shield against either the service instance becoming unavailable or the resource becoming unavailable within the service instance (due to some URI restructuring or the like). The calls to verifyOrDiscover() are currently automated by a RemoteResourceRefresher which is auto-configured if a RemoteResource is declared as Spring bean and its initial and fixed delay can be configured via properties in the spring.cloud.hypermedia namespace. --- spring-cloud-commons/pom.xml | 10 ++ .../CloudHypermediaAutoConfiguration.java | 66 ++++++++++ .../client/hypermedia/DiscoveredResource.java | 121 ++++++++++++++++++ .../DynamicServiceInstanceProvider.java | 53 ++++++++ .../client/hypermedia/RemoteResource.java | 40 ++++++ .../hypermedia/RemoteResourceRefresher.java | 57 +++++++++ .../hypermedia/ServiceInstanceProvider.java | 34 +++++ .../StaticServiceInstanceProvider.java | 40 ++++++ .../hypermedia/TraversalDefinition.java | 33 +++++ .../main/resources/META-INF/spring.factories | 1 + ...ediaAutoConfigurationIntegrationTests.java | 105 +++++++++++++++ .../DiscoveredResourceUnitTests.java | 108 ++++++++++++++++ ...namicServiceInstanceProviderUnitTests.java | 55 ++++++++ .../src/test/resources/application.properties | 5 +- 14 files changed, 727 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfiguration.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DiscoveredResource.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProvider.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResource.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/RemoteResourceRefresher.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/ServiceInstanceProvider.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/StaticServiceInstanceProvider.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/hypermedia/TraversalDefinition.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/CloudHypermediaAutoConfigurationIntegrationTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DiscoveredResourceUnitTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/hypermedia/DynamicServiceInstanceProviderUnitTests.java 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