diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java index 0a74b66e..416e234d 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java @@ -16,16 +16,28 @@ package org.springframework.cloud.zookeeper.config; +import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import org.apache.commons.logging.Log; import org.apache.curator.framework.CuratorFramework; import org.springframework.boot.context.config.ConfigData; import org.springframework.boot.context.config.ConfigDataLoader; import org.springframework.boot.context.config.ConfigDataLoaderContext; import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; +import org.springframework.util.StringUtils; public class ZookeeperConfigDataLoader implements ConfigDataLoader { + private static final EnumSet ALL_OPTIONS = EnumSet.allOf(ConfigData.Option.class); + + private final Log log; + + public ZookeeperConfigDataLoader(Log log) { + this.log = log; + } @Override public ConfigData load(ConfigDataLoaderContext context, ZookeeperConfigDataResource resource) { @@ -33,11 +45,36 @@ public class ZookeeperConfigDataLoader implements ConfigDataLoader propertySources = Collections.singletonList(propertySource); + if (ALL_OPTIONS.size() == 1) { + // boot 2.4.2 and prior + return new ConfigData(propertySources); + } + else if (ALL_OPTIONS.size() == 2) { + // boot 2.4.3 and 2.4.4 + return new ConfigData(propertySources, ConfigData.Option.IGNORE_IMPORTS, ConfigData.Option.IGNORE_PROFILES); + } + else if (ALL_OPTIONS.size() > 2) { + // boot 2.4.5+ + return new ConfigData(propertySources, source -> { + List options = new ArrayList<>(); + options.add(ConfigData.Option.IGNORE_IMPORTS); + options.add(ConfigData.Option.IGNORE_PROFILES); + if (StringUtils.hasText(resource.getProfile())) { + options.add(ConfigData.Option.PROFILE_SPECIFIC); + } + return ConfigData.Options.of(options.toArray(new ConfigData.Option[0])); + }); + + } } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Error getting properties from consul: " + resource, e); + } throw new ConfigDataResourceNotFoundException(resource, e); } + return null; } } 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 809b9f58..725a11de 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 @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -35,6 +36,7 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.zookeeper.CuratorFactory; import org.springframework.cloud.zookeeper.ZookeeperProperties; +import org.springframework.cloud.zookeeper.config.ZookeeperPropertySources.Context; import org.springframework.core.env.MapPropertySource; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -88,20 +90,21 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe ZookeeperPropertySources sources = new ZookeeperPropertySources(properties, log); - List contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments())) - ? sources.getAutomaticContexts(profiles.getAccepted(), false) : getCustomContexts(locationUri); + List contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments())) + ? sources.generateAutomaticContexts(profiles.getAccepted(), false) : getCustomContexts(locationUri); // promote beans to context context.getBootstrapContext().addCloseListener(event -> { HashMap source = new HashMap<>(); - source.put("spring.cloud.zookeeper.config.property-source-contexts", contexts); + source.put("spring.cloud.zookeeper.config.property-source-contexts", contexts.stream().map(Context::getPath).collect(Collectors.toList())); MapPropertySource propertySource = new MapPropertySource("zookeeperConfigData", source); event.getApplicationContext().getEnvironment().getPropertySources().addFirst(propertySource); }); ArrayList locations = new ArrayList<>(); contexts.forEach(propertySourceContext -> locations - .add(new ZookeeperConfigDataResource(propertySourceContext, location.isOptional()))); + .add(new ZookeeperConfigDataResource(propertySourceContext.getPath(), location.isOptional(), propertySourceContext + .getProfile()))); return locations; } @@ -110,12 +113,12 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe return context.getBootstrapContext().getOrElse(BindHandler.class, null); } - protected List getCustomContexts(UriComponents uriComponents) { + protected List getCustomContexts(UriComponents uriComponents) { if (!StringUtils.hasLength(uriComponents.getPath())) { return Collections.emptyList(); } - return Arrays.asList(uriComponents.getPath().split(";")); + return Arrays.stream(uriComponents.getPath().split(";")).map(Context::new).collect(Collectors.toList()); } @Nullable diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataResource.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataResource.java index 9a1b8f37..abb58500 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataResource.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataResource.java @@ -25,10 +25,17 @@ public class ZookeeperConfigDataResource extends ConfigDataResource { private final String context; private final boolean optional; + private final String profile; - public ZookeeperConfigDataResource(String context, boolean optional) { + public ZookeeperConfigDataResource(String context, boolean optional, String profile) { this.context = context; this.optional = optional; + this.profile = profile; + } + + @Deprecated + public ZookeeperConfigDataResource(String context, boolean optional) { + this(context, optional, null); } public String getContext() { @@ -39,6 +46,10 @@ public class ZookeeperConfigDataResource extends ConfigDataResource { return this.optional; } + public String getProfile() { + return this.profile; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -48,13 +59,12 @@ public class ZookeeperConfigDataResource extends ConfigDataResource { return false; } ZookeeperConfigDataResource that = (ZookeeperConfigDataResource) o; - return this.optional == that.optional && - this.context.equals(that.context); + return this.optional == that.optional && this.context.equals(that.context) && Objects.equals(this.profile, that.profile); } @Override public int hashCode() { - return Objects.hash(this.optional, this.context); + return Objects.hash(this.optional, this.context, this.profile); } @Override @@ -62,6 +72,7 @@ public class ZookeeperConfigDataResource extends ConfigDataResource { return new ToStringCreator(this) .append("context", context) .append("optional", optional) + .append("profile", profile) .toString(); } diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySources.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySources.java index 1cf8409a..0730ff1b 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySources.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySources.java @@ -19,10 +19,13 @@ package org.springframework.cloud.zookeeper.config; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.curator.framework.CuratorFramework; +import org.springframework.core.style.ToStringCreator; + public class ZookeeperPropertySources { private final ZookeeperConfigProperties properties; private final Log log; @@ -37,11 +40,15 @@ public class ZookeeperPropertySources { } public List getAutomaticContexts(List profiles, boolean reverse) { + return generateAutomaticContexts(profiles, reverse).stream().map(Context::getPath).collect(Collectors.toList()); + } + + public List generateAutomaticContexts(List profiles, boolean reverse) { + List contexts = new ArrayList<>(); String root = properties.getRoot(); - List contexts = new ArrayList<>(); String defaultContext = root + "/" + properties.getDefaultContext(); - contexts.add(defaultContext); + contexts.add(new Context(defaultContext)); addProfiles(contexts, defaultContext, profiles); StringBuilder baseContext = new StringBuilder(root); @@ -50,7 +57,7 @@ public class ZookeeperPropertySources { } // getName() defaults to ${spring.application.name} or application baseContext.append(properties.getName()); - contexts.add(baseContext.toString()); + contexts.add(new Context(baseContext.toString())); addProfiles(contexts, baseContext.toString(), profiles); if (reverse) { @@ -59,9 +66,10 @@ public class ZookeeperPropertySources { return contexts; } - private void addProfiles(List contexts, String baseContext, List profiles) { + private void addProfiles(List contexts, String baseContext, List profiles) { for (String profile : profiles) { - contexts.add(baseContext + properties.getProfileSeparator() + profile); + String path = baseContext + properties.getProfileSeparator() + profile; + contexts.add(new Context(path, profile)); } } @@ -80,6 +88,38 @@ public class ZookeeperPropertySources { } return null; } + public static class Context { + + private final String path; + + private final String profile; + + public Context(String path) { + this.path = path; + this.profile = null; + } + + public Context(String path, String profile) { + this.path = path; + this.profile = profile; + } + + public String getPath() { + return this.path; + } + + public String getProfile() { + return this.profile; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("path", path).append("profile", profile).toString(); + + } + + } + static class ZookeeperPropertySourceNotFoundException extends RuntimeException { diff --git a/spring-cloud-zookeeper-sample/pom.xml b/spring-cloud-zookeeper-sample/pom.xml index c8f8649b..c98fd0a7 100644 --- a/spring-cloud-zookeeper-sample/pom.xml +++ b/spring-cloud-zookeeper-sample/pom.xml @@ -67,6 +67,13 @@ curator-test test + + org.springframework.cloud + spring-cloud-zookeeper-core + ${project.version} + test-jar + test + diff --git a/spring-cloud-zookeeper-sample/src/test/java/org/springframework/cloud/zookeeper/sample/ZookeeperConfigDataOrderingIntegrationTests.java b/spring-cloud-zookeeper-sample/src/test/java/org/springframework/cloud/zookeeper/sample/ZookeeperConfigDataOrderingIntegrationTests.java new file mode 100644 index 00000000..8fc2d592 --- /dev/null +++ b/spring-cloud-zookeeper-sample/src/test/java/org/springframework/cloud/zookeeper/sample/ZookeeperConfigDataOrderingIntegrationTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2018-2021 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.sample; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.imps.CuratorFrameworkState; +import org.apache.curator.retry.RetryOneTime; +import org.apache.zookeeper.KeeperException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.zookeeper.ZookeeperProperties; +import org.springframework.cloud.zookeeper.config.ZookeeperConfigProperties; +import org.springframework.cloud.zookeeper.test.ZookeeperTestingServer; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(classes = SampleZookeeperApplication.class, + properties = { "spring.application.name=" + ZookeeperConfigDataOrderingIntegrationTests.APP_NAME, + "spring.config.name=orderingtest", "spring.profiles.active=dev", + "management.endpoints.web.exposure.include=*" }, + webEnvironment = RANDOM_PORT) +public class ZookeeperConfigDataOrderingIntegrationTests { + + private static final String BASE_PATH = new WebEndpointProperties().getBasePath(); + + static final String APP_NAME = "testZkConfigDataOrderingIntegration"; + + private static final String PREFIX = "_configDataOrderingIntegrationTests_config__"; + + private static final String ROOT = "/" + PREFIX + UUID.randomUUID(); + + private static final String VALUE = "my value from zk default profile"; + + private static final String TEST_PROP = "my.prop"; + + private static final String KEY = ROOT + "/" + APP_NAME + "/" + TEST_PROP; + + private static final String VALUE_PROFILE = "my value from zk dev profile"; + + private static final String KEY_PROFILE = ROOT + "/" + APP_NAME + ",dev/" + TEST_PROP; + private static ZookeeperTestingServer testingServer; + private static CuratorFramework curator; + + @Autowired + private Environment env; + + @BeforeAll + public static void initialize() throws Exception { + testingServer = new ZookeeperTestingServer(); + testingServer.start(); + System.setProperty(ZookeeperProperties.PREFIX + ".connect-string", "localhost:" + testingServer.getPort()); + System.setProperty(ZookeeperConfigProperties.PREFIX + ".root", ROOT); + String connectString = "localhost:" + testingServer.getPort(); + curator = CuratorFrameworkFactory.builder() + .retryPolicy(new RetryOneTime(500)).connectString(connectString).build(); + curator.start(); + List children = curator.getChildren().forPath("/"); + for (String child : children) { + if (child.startsWith(PREFIX) && child.length() > PREFIX.length()) { + delete("/" + child); + } + } + + StringBuilder create = new StringBuilder(1024); + create.append(curator.create().creatingParentsIfNeeded() + .forPath(KEY, VALUE.getBytes())).append('\n'); + create.append(curator.create().creatingParentsIfNeeded() + .forPath(KEY_PROFILE, VALUE_PROFILE.getBytes())).append('\n'); + curator.close(); + System.out.println(create); + } + + public static void delete(String path) throws Exception { + try { + if (curator.getState() == CuratorFrameworkState.STARTED) { + curator.delete().deletingChildrenIfNeeded().forPath(path); + } + } + catch (KeeperException e) { + if (e.code() != KeeperException.Code.NONODE) { + throw e; + } + } + } + @AfterAll + public static void close() throws Exception { + try { + delete(ROOT); + } + finally { + testingServer.close(); + } + System.clearProperty(ZookeeperProperties.PREFIX + ".connect-string"); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void contextLoads() { + Integer port = env.getProperty("local.server.port", Integer.class); + ResponseEntity response = new TestRestTemplate() + .getForEntity("http://localhost:" + port + BASE_PATH + "/env/my.prop", Map.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + Map res = response.getBody(); + assertThat(res).containsKey("propertySources"); + Map property = (Map) res.get("property"); + assertThat(property).containsEntry("value", VALUE_PROFILE); + } + +} diff --git a/spring-cloud-zookeeper-sample/src/test/resources/orderingtest-dev.properties b/spring-cloud-zookeeper-sample/src/test/resources/orderingtest-dev.properties new file mode 100644 index 00000000..9ca92ca8 --- /dev/null +++ b/spring-cloud-zookeeper-sample/src/test/resources/orderingtest-dev.properties @@ -0,0 +1 @@ +my.prop=my value from local dev profile diff --git a/spring-cloud-zookeeper-sample/src/test/resources/orderingtest.properties b/spring-cloud-zookeeper-sample/src/test/resources/orderingtest.properties new file mode 100644 index 00000000..f094a890 --- /dev/null +++ b/spring-cloud-zookeeper-sample/src/test/resources/orderingtest.properties @@ -0,0 +1 @@ +spring.config.import=zookeeper: