diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java index 9e50a141..6bc09255 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java @@ -17,10 +17,12 @@ package org.springframework.cloud.consul.config; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; import com.ecwid.consul.v1.ConsulClient; @@ -32,13 +34,21 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.consul.ConsulAutoConfiguration; import org.springframework.cloud.consul.ConsulProperties; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES; public class ConsulConfigDataLocationResolver implements ConfigDataLocationResolver { + protected static final List DIR_SUFFIXES = Collections.singletonList("/"); + + protected static final List FILES_SUFFIXES = Collections + .unmodifiableList(Arrays.asList(".yml", ".yaml", ".properties")); + @Override public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { @@ -62,7 +72,10 @@ public class ConsulConfigDataLocationResolver ConfigDataLocationResolverContext context, String location, boolean optional, Profiles profiles) throws ConfigDataLocationNotFoundException { - ConsulConfigProperties properties = loadConfigProperties(context.getBinder()); + UriComponents locationUri = parseLocation(context, location); + + ConsulConfigProperties properties = loadConfigProperties(context.getBinder(), + locationUri); String appName = properties.getName(); if (StringUtils.isEmpty(appName)) { @@ -70,74 +83,68 @@ public class ConsulConfigDataLocationResolver .orElse("application"); } - String prefix = properties.getPrefix(); - List suffixes = new ArrayList<>(); - if (properties.getFormat() != FILES) { - suffixes.add("/"); - } - else { - suffixes.add(".yml"); - suffixes.add(".yaml"); - suffixes.add(".properties"); + List contexts = (CollectionUtils.isEmpty(locationUri.getPathSegments())) + ? getAutomaticContexts(profiles, properties, appName) + : getCustomContexts(locationUri, properties); + + registerBean(context, ConsulClient.class, + () -> createConsulClient(context, locationUri)); + + registerBean(context, ConsulConfigIndexes.class, ConsulConfigDataIndexes::new); + + return contexts.stream() + .map(propertySourceContext -> new ConsulConfigDataLocation(properties, + propertySourceContext, optional)) + .collect(Collectors.toList()); + } + + private List getCustomContexts(UriComponents uriComponents, + ConsulConfigProperties properties) { + List contexts = new ArrayList<>(); + if (StringUtils.isEmpty(uriComponents.getPath())) { + return contexts; } - String defaultContext = getContext(prefix, properties.getDefaultContext()); + for (String path : uriComponents.getPath().split(";")) { + for (String suffix : getSuffixes(properties)) { + contexts.add(path + suffix); + } + } + return contexts; + } + + protected List getSuffixes(ConsulConfigProperties properties) { + if (properties.getFormat() == FILES) { + return FILES_SUFFIXES; + } + return DIR_SUFFIXES; + } + + protected List getAutomaticContexts(Profiles profiles, + ConsulConfigProperties properties, String appName) { List contexts = new ArrayList<>(); - for (String suffix : suffixes) { + String prefix = properties.getPrefix(); + String defaultContext = getContext(prefix, properties.getDefaultContext()); + for (String suffix : getSuffixes(properties)) { contexts.add(defaultContext + suffix); } - for (String suffix : suffixes) { + for (String suffix : getSuffixes(properties)) { addProfiles(contexts, defaultContext, profiles, suffix, properties); } String baseContext = getContext(prefix, appName); - for (String suffix : suffixes) { + for (String suffix : getSuffixes(properties)) { contexts.add(baseContext + suffix); } - for (String suffix : suffixes) { + for (String suffix : getSuffixes(properties)) { addProfiles(contexts, baseContext, profiles, suffix, properties); } - + // we build them backwards, first wins, so reverse Collections.reverse(contexts); - - registerBean(context, ConsulClient.class, () -> createConsulClient(context, location)); - - registerBean(context, ConsulConfigIndexes.class, ConsulConfigDataIndexes::new); - - ArrayList locations = new ArrayList<>(); - contexts.forEach( - propertySourceContext -> locations.add(new ConsulConfigDataLocation( - properties, propertySourceContext, optional))); - - return locations; - } - - protected void registerBean(ConfigDataLocationResolverContext context, - Class type, Supplier supplier) { - context.getBootstrapRegistry().register(type, supplier) - .onApplicationContextPrepared( - (ctxt, consulClient) -> ctxt.getBeanFactory().registerSingleton( - "configData" + type.getSimpleName(), consulClient)); - } - - protected ConsulClient createConsulClient(ConfigDataLocationResolverContext context, String location) { - ConsulProperties properties = loadProperties(context.getBinder()); - - String hostPort = location.substring("consul:".length()); - if (StringUtils.hasText(hostPort)) { - String[] split = hostPort.split(":"); - if (split.length == 2) { // host and port - properties.setHost(split[0]); - properties.setPort(Integer.parseInt(split[1])); - } - } - - //TODO: add support for ConsulConfigProperties.prefix - - return ConsulAutoConfiguration.createConsulClient(properties); + return contexts; } protected String getContext(String prefix, String context) { @@ -157,12 +164,52 @@ public class ConsulConfigDataLocationResolver } } - protected ConsulProperties loadProperties(Binder binder) { - return binder.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class)) - .orElse(new ConsulProperties()); + protected UriComponents parseLocation(ConfigDataLocationResolverContext context, + String location) { + String uri = location.substring("consul:".length()); + if (!StringUtils.hasText(uri)) { + return null; + } + if (!uri.startsWith("//")) { + uri = "consul:" + "//" + uri; + } + else { + uri = location; + } + return UriComponentsBuilder.fromUriString(uri).build(); } - protected ConsulConfigProperties loadConfigProperties(Binder binder) { + protected void registerBean(ConfigDataLocationResolverContext context, + Class type, Supplier supplier) { + context.getBootstrapRegistry().register(type, supplier) + .onApplicationContextPrepared( + (ctxt, consulClient) -> ctxt.getBeanFactory().registerSingleton( + "configData" + type.getSimpleName(), consulClient)); + } + + protected ConsulClient createConsulClient(ConfigDataLocationResolverContext context, + UriComponents location) { + ConsulProperties properties = loadProperties(context.getBinder(), location); + return ConsulAutoConfiguration.createConsulClient(properties); + } + + protected ConsulProperties loadProperties(Binder binder, UriComponents location) { + ConsulProperties consulProperties = binder + .bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class)) + .orElse(new ConsulProperties()); + + if (StringUtils.hasText(location.getHost())) { + consulProperties.setHost(location.getHost()); + } + if (location.getPort() >= 0) { + consulProperties.setPort(location.getPort()); + } + + return consulProperties; + } + + protected ConsulConfigProperties loadConfigProperties(Binder binder, + UriComponents location) { return binder .bind(ConsulConfigProperties.PREFIX, Bindable.of(ConsulConfigProperties.class)) diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java index 1a428613..9ddd9de4 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java @@ -89,8 +89,8 @@ public class ConsulConfigDataIntegrationTests { context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE) .run("--spring.application.name=" + APP_NAME, - "--spring.config.import=consul:" + ConsulTestcontainers.getHost() + ":" + - ConsulTestcontainers.getPort(), + "--spring.config.import=consul:" + ConsulTestcontainers.getHost() + + ":" + ConsulTestcontainers.getPort(), "--spring.cloud.consul.config.prefix=" + ROOT, "--spring.cloud.consul.config.watch.delay=10"); diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolverTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolverTests.java new file mode 100644 index 00000000..e126df24 --- /dev/null +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolverTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2020 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 + * + * https://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.consul.config; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.consul.ConsulProperties; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConsulConfigDataLocationResolverTests { + + @Test + public void testParseLocation() { + ConsulConfigDataLocationResolver resolver = new ConsulConfigDataLocationResolver(); + UriComponents uriComponents = resolver.parseLocation(null, + "consul:myhost:8501/mypath1;/mypath2;/mypath3"); + assertThat(uriComponents.toUri()).hasScheme("consul").hasHost("myhost") + .hasPort(8501).hasPath("/mypath1;/mypath2;/mypath3"); + + uriComponents = resolver.parseLocation(null, "consul:myhost:8501"); + assertThat(uriComponents.toUri()).hasScheme("consul").hasHost("myhost") + .hasPort(8501).hasPath(""); + } + + @Test + public void testResolveProfileSpecificWithCustomPaths() { + String location = "consul:myhost:8501/mypath1;/mypath2;/mypath3"; + List locations = testResolveProfileSpecific(location); + assertThat(locations).hasSize(3); + assertThat(toContexts(locations)).containsExactly("/mypath1/", "/mypath2/", + "/mypath3/"); + } + + @Test + public void testResolveProfileSpecificWithAutomaticPaths() { + String location = "consul:myhost"; + List locations = testResolveProfileSpecific(location); + assertThat(locations).hasSize(4); + assertThat(toContexts(locations)).containsExactly("config/testapp,dev/", + "config/testapp/", "config/application,dev/", "config/application/"); + } + + @Test + public void testLoadProperties() { + ConsulProperties properties = createResolver().loadProperties( + Binder.get(new MockEnvironment()), + UriComponentsBuilder.fromUriString("consul://myhost:8502").build()); + assertThat(properties.getHost()).isEqualTo("myhost"); + assertThat(properties.getPort()).isEqualTo(8502); + } + + private List toContexts(List locations) { + return locations.stream().map(ConsulConfigDataLocation::getContext) + .collect(Collectors.toList()); + } + + private List testResolveProfileSpecific(String location) { + ConsulConfigDataLocationResolver resolver = createResolver(); + ConfigDataLocationResolverContext context = mock( + ConfigDataLocationResolverContext.class); + MockEnvironment env = new MockEnvironment(); + env.setProperty("spring.application.name", "testapp"); + when(context.getBinder()).thenReturn(Binder.get(env)); + Profiles profiles = mock(Profiles.class); + when(profiles.getAccepted()).thenReturn(Collections.singletonList("dev")); + return resolver.resolveProfileSpecific(context, location, false, profiles); + } + + private ConsulConfigDataLocationResolver createResolver() { + ConsulConfigDataLocationResolver resolver = new ConsulConfigDataLocationResolver() { + @Override + protected void registerBean(ConfigDataLocationResolverContext context, + Class type, Supplier supplier) { + // do nothing + } + }; + return resolver; + } + +}