diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java index 2b17072f0f..4e83fd1112 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java @@ -438,6 +438,28 @@ private Map convertToProperties(Environment profiles) { private void postProcessProperties(Map propertiesMap) { propertiesMap.keySet().removeIf(key -> key.equals("spring.profiles")); + restoreEmptyCollections(propertiesMap); + } + + /** + * Flattened YAML properties represent empty collections as an empty string (see + * {@code YamlProcessor#buildFlattenedMap}). When rebuilding nested YAML/JSON, restore + * those markers to empty lists unless indexed entries exist for the same prefix. + */ + private void restoreEmptyCollections(Map propertiesMap) { + Set arrayPrefixes = new HashSet<>(); + for (String key : propertiesMap.keySet()) { + int indexOfBracket = key.indexOf('['); + if (indexOfBracket > 0) { + arrayPrefixes.add(key.substring(0, indexOfBracket)); + } + } + for (Entry entry : propertiesMap.entrySet()) { + String key = entry.getKey(); + if (!key.contains("[") && "".equals(entry.getValue()) && !arrayPrefixes.contains(key)) { + entry.setValue(Collections.emptyList()); + } + } } /** diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java index f29f28a4cb..217511d135 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java @@ -197,6 +197,26 @@ public void arrayInYaml() throws Exception { assertThat(yaml).isEqualTo("a:\n b:\n - c\n - d\n"); } + @Test + public void emptyArrayInJson() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("network.urls", ""); + this.environment.add(new PropertySource("one", map)); + when(this.repository.findOne("foo", "bar", null, false)).thenReturn(this.environment); + String json = this.controller.jsonProperties("foo", "bar", false).getBody(); + JSONAssert.assertEquals("{\"network\":{\"urls\":[]}}", json, JSONCompareMode.STRICT); + } + + @Test + public void emptyArrayInYaml() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("network.urls", ""); + this.environment.add(new PropertySource("one", map)); + when(this.repository.findOne("foo", "bar", null, false)).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar", false).getBody(); + assertThat(yaml).isEqualTo("network:\n urls: []\n"); + } + @Test public void yamlWithBrackets() throws Exception { Map map = new LinkedHashMap(); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java index d592abad96..6bbd99bb3f 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -30,6 +32,7 @@ import org.springframework.cloud.config.environment.PropertySource; import org.springframework.cloud.config.server.environment.SearchPathLocator.Locations; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -127,6 +130,26 @@ public void prefixedMultiDocProperties() { .isNotEqualTo(environment.getPropertySources().get(1).getName()); } + @Test + public void emptyYamlArrayInJsonOutput() throws Exception { + this.repository.setSearchLocations("classpath:/test/empty-array"); + Environment environment = this.repository.findOne("application", "default", "master"); + EnvironmentRepository repository = new EnvironmentRepository() { + @Override + public Environment findOne(String application, String profile, String label) { + return environment; + } + + @Override + public Environment findOne(String application, String profile, String label, boolean includeOrigin) { + return environment; + } + }; + EnvironmentController controller = new EnvironmentController(repository); + ResponseEntity json = controller.labelledJsonProperties("application", "default", "master", false); + JSONAssert.assertEquals("{\"network\":{\"urls\":[]}}", json.getBody(), JSONCompareMode.STRICT); + } + @Test public void prefixedWithFile() { this.repository.setSearchLocations("file:./src/test/resources/test"); diff --git a/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml b/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml new file mode 100644 index 0000000000..50882e6205 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml @@ -0,0 +1,2 @@ +network: + urls: []