From 2b759686d97f49fdfed487615fb558a68ab4f4c9 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 9 Sep 2020 23:25:49 -0400 Subject: [PATCH] Adds support for paths in ConfigData Now `zookeeper:myhost:8501/mypath1;/mypath2;/mypath3` will use the paths separated by semicolon (;) instead of automatically generating paths. `zookeeper:myhost:8501` or `zookeeper:` will automatically generate paths from ZookeeperConfigProperties and active profiles. --- .../ZookeeperConfigDataLocationResolver.java | 99 +++++++++++----- .../config/ZookeeperConfigProperties.java | 58 ++++++--- .../ZookeeperPropertySourceLocator.java | 12 +- ...keeperConfigDataLocationResolverTests.java | 110 ++++++++++++++++++ 4 files changed, 231 insertions(+), 48 deletions(-) create mode 100644 spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolverTests.java diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java index 3c446716..c736e983 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java @@ -17,6 +17,7 @@ package org.springframework.cloud.zookeeper.config; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -36,19 +37,28 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.zookeeper.ZookeeperProperties; import org.springframework.core.env.MapPropertySource; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationResolver { private static final Log log = LogFactory.getLog(ZookeeperConfigDataLocationResolver.class); + /** + * Zookeeper Config Data prefix. + */ + public static final String PREFIX = "zookeeper:"; + @Override public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { boolean zkEnabled = context.getBinder().bind(ZookeeperProperties.PREFIX + ".enabled", Boolean.class) .orElse(true); boolean zkConfigEnabled = context.getBinder().bind(ZookeeperConfigProperties.PREFIX + ".enabled", Boolean.class) .orElse(true); - return location.startsWith("zookeeper:") && zkConfigEnabled && zkEnabled; + return location.startsWith(PREFIX) && zkConfigEnabled && zkEnabled; } @Override @@ -60,29 +70,16 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe @Override public List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional, Profiles profiles) throws ConfigDataLocationNotFoundException { - - String appName = context.getBinder().bind("spring.application.name", String.class).orElse("application"); + UriComponents locationUri = parseLocation(context, location); ZookeeperConfigProperties properties = loadConfigProperties(context.getBinder()); - String root = properties.getRoot(); - List contexts = new ArrayList<>(); - String defaultContext = root + "/" + properties.getDefaultContext(); - contexts.add(defaultContext); - addProfiles(contexts, defaultContext, profiles, properties); - - StringBuilder baseContext = new StringBuilder(root); - if (!appName.startsWith("/")) { - baseContext.append("/"); - } - baseContext.append(appName); - contexts.add(baseContext.toString()); - addProfiles(contexts, baseContext.toString(), profiles, properties); - - Collections.reverse(contexts); + List contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments())) + ? getAutomaticContexts(profiles, properties) : getCustomContexts(locationUri, properties); context.getBootstrapRegistry() - .register(CuratorFramework.class, () -> curatorFramework(optional, loadProperties(context, location))) + .register(CuratorFramework.class, + () -> curatorFramework(optional, loadProperties(context.getBinder(), locationUri))) .onApplicationContextPrepared((ctxt, curatorFramework) -> { ctxt.getBeanFactory().registerSingleton("configDataCuratorFramework", curatorFramework); HashMap source = new HashMap<>(); @@ -98,6 +95,50 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe return locations; } + protected List getCustomContexts(UriComponents uriComponents, ZookeeperConfigProperties properties) { + if (StringUtils.isEmpty(uriComponents.getPath())) { + return Collections.emptyList(); + } + + return Arrays.asList(uriComponents.getPath().split(";")); + } + + protected List getAutomaticContexts(Profiles profiles, ZookeeperConfigProperties properties) { + String root = properties.getRoot(); + List contexts = new ArrayList<>(); + + String defaultContext = root + "/" + properties.getDefaultContext(); + contexts.add(defaultContext); + addProfiles(contexts, defaultContext, profiles, properties); + + StringBuilder baseContext = new StringBuilder(root); + if (!properties.getName().startsWith("/")) { + baseContext.append("/"); + } + // getName() defaults to ${spring.application.name} or application + baseContext.append(properties.getName()); + contexts.add(baseContext.toString()); + addProfiles(contexts, baseContext.toString(), profiles, properties); + + Collections.reverse(contexts); + return contexts; + } + + @Nullable + protected UriComponents parseLocation(ConfigDataLocationResolverContext context, String location) { + String uri = location.substring(PREFIX.length()); + if (!StringUtils.hasText(uri)) { + return null; + } + if (!uri.startsWith("//")) { + uri = PREFIX + "//" + uri; + } + else { + uri = location; + } + return UriComponentsBuilder.fromUriString(uri).build(); + } + private void addProfiles(List contexts, String baseContext, Profiles profiles, ZookeeperConfigProperties properties) { for (String profile : profiles.getAccepted()) { @@ -144,18 +185,17 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe properties.getMaxSleepMs()); } - private ZookeeperProperties loadProperties(ConfigDataLocationResolverContext context, String location) { - ZookeeperProperties properties = context.getBinder() - .bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class)) + protected ZookeeperProperties loadProperties(Binder binder, UriComponents location) { + ZookeeperProperties properties = binder.bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class)) .orElse(new ZookeeperProperties()); - String connectString = location.substring("zookeeper:".length()); - if (StringUtils.hasText(connectString)) { - properties.setConnectString(connectString); + if (location != null && StringUtils.hasText(location.getHost())) { + if (location.getPort() < 0) { + throw new IllegalArgumentException("zookeeper port must be greater than or equal to zero: " + location.getPort()); + } + properties.setConnectString(location.getHost() + ":" + location.getPort()); } - // TODO: support ZookeeperConfigProperties.root - return properties; } @@ -163,6 +203,11 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe ZookeeperConfigProperties properties = binder .bind(ZookeeperConfigProperties.PREFIX, Bindable.of(ZookeeperConfigProperties.class)) .orElse(new ZookeeperConfigProperties()); + + if (StringUtils.isEmpty(properties.getName())) { + properties.setName(binder.bind("spring.application.name", String.class).orElse("application")); + } + return properties; } diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java index b14bba28..ae55f1c1 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java @@ -17,6 +17,7 @@ package org.springframework.cloud.zookeeper.config; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; /** @@ -41,6 +42,11 @@ public class ZookeeperConfigProperties { */ private String root = "config"; + /** + * Alternative to spring.application.name to use in looking up values in zookeeper. + */ + private String name; + /** * The name of the default context. */ @@ -60,42 +66,62 @@ public class ZookeeperConfigProperties { return this.enabled; } - public String getRoot() { - return this.root; - } - - public String getDefaultContext() { - return this.defaultContext; - } - - public String getProfileSeparator() { - return this.profileSeparator; - } - - public boolean isFailFast() { - return this.failFast; - } - public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getRoot() { + return this.root; + } + public void setRoot(String root) { this.root = root; } + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDefaultContext() { + return this.defaultContext; + } + public void setDefaultContext(String defaultContext) { Assert.hasText(defaultContext, "spring.cloud.zookeeper.config.default-context may not be empty"); this.defaultContext = defaultContext; } + public String getProfileSeparator() { + return this.profileSeparator; + } + public void setProfileSeparator(String profileSeparator) { Assert.hasText(profileSeparator, "spring.cloud.zookeeper.config.profile-separator may not be empty"); this.profileSeparator = profileSeparator; } + public boolean isFailFast() { + return this.failFast; + } + public void setFailFast(boolean failFast) { this.failFast = failFast; } + @Override + public String toString() { + return new ToStringCreator(this) + .append("enabled", enabled) + .append("root", root) + .append("name", name) + .append("defaultContext", defaultContext) + .append("profileSeparator", profileSeparator) + .append("failFast", failFast) + .toString(); + + } } diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocator.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocator.java index 74ccc207..82ca7e59 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocator.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocator.java @@ -33,6 +33,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * Zookeeper provides a locate(Environment environment) { if (environment instanceof ConfigurableEnvironment) { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; - String appName = env.getProperty("spring.application.name"); - if (appName == null) { + String appName = properties.getName(); + if (StringUtils.isEmpty(appName)) { // use default "application" (which config client does) - appName = "application"; - log.warn( - "spring.application.name is not set. Using default of 'application'"); + appName = env.getProperty("spring.application.name", "application"); + if (appName.equals("application")) { + log.warn("spring.application.name is not set. Using default of 'application'"); + } } List profiles = Arrays.asList(env.getActiveProfiles()); diff --git a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolverTests.java b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolverTests.java new file mode 100644 index 00000000..5e3650a9 --- /dev/null +++ b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolverTests.java @@ -0,0 +1,110 @@ +/* + * 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.zookeeper.config; + +import java.util.Collections; +import java.util.List; +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.boot.env.BootstrapRegistry; +import org.springframework.cloud.zookeeper.ZookeeperProperties; +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.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ZookeeperConfigDataLocationResolverTests { + + @Test + public void testParseLocation() { + ZookeeperConfigDataLocationResolver resolver = new ZookeeperConfigDataLocationResolver(); + UriComponents uriComponents = resolver.parseLocation(null, + "zookeeper:myhost:2182/mypath1;/mypath2;/mypath3"); + assertThat(uriComponents.toUri()).hasScheme("zookeeper").hasHost("myhost") + .hasPort(2182).hasPath("/mypath1;/mypath2;/mypath3"); + + uriComponents = resolver.parseLocation(null, "zookeeper:myhost:2182"); + assertThat(uriComponents.toUri()).hasScheme("zookeeper").hasHost("myhost") + .hasPort(2182).hasPath(""); + } + + @Test + public void testResolveProfileSpecificWithCustomPaths() { + String location = "zookeeper:myhost:2182/mypath1;/mypath2;/mypath3"; + List locations = testResolveProfileSpecific(location); + assertThat(locations).hasSize(3); + assertThat(toContexts(locations)).containsExactly("/mypath1", "/mypath2", + "/mypath3"); + } + + @Test + public void testResolveProfileSpecificWithAutomaticPaths() { + String location = "zookeeper: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() { + ZookeeperProperties properties = createResolver().loadProperties( + Binder.get(new MockEnvironment()), + UriComponentsBuilder.fromUriString("zookeeper://myhost:8502").build()); + assertThat(properties.getConnectString()).isEqualTo("myhost:8502"); + } + + private List toContexts(List locations) { + return locations.stream().map(ZookeeperConfigDataLocation::getContext) + .collect(Collectors.toList()); + } + + private List testResolveProfileSpecific(String location) { + ZookeeperConfigDataLocationResolver resolver = createResolver(); + + MockEnvironment env = new MockEnvironment(); + env.setProperty("spring.application.name", "testapp"); + + BootstrapRegistry registry = mock(BootstrapRegistry.class); + when(registry.register(any(), any())).thenReturn(mock(BootstrapRegistry.Registration.class)); + + ConfigDataLocationResolverContext context = mock( + ConfigDataLocationResolverContext.class); + + when(context.getBootstrapRegistry()).thenReturn(registry); + 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 ZookeeperConfigDataLocationResolver createResolver() { + return new ZookeeperConfigDataLocationResolver(); + } + +}