From 5bb361e8067afb4b89b4f086fedae0bebe764e2c Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 21 Oct 2014 13:34:42 -0700 Subject: [PATCH] Add .properties and .yml endpoint --- .../config/server/EnvironmentController.java | 163 +++++++++++++++++- .../server/EnvironmentControllerTests.java | 128 ++++++++++++++ 2 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java index 49073e48..a0068921 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/EnvironmentController.java @@ -1,17 +1,36 @@ - package org.springframework.cloud.config.server; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.servlet.http.HttpServletResponse; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.cloud.config.Environment; +import org.springframework.cloud.config.PropertySource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.yaml.snakeyaml.Yaml; @RestController public class EnvironmentController { private EnvironmentRepository repository; - + private EncryptionController encryption; @Autowired @@ -22,14 +41,142 @@ public class EnvironmentController { this.encryption = encryption; } - @RequestMapping("/{name}/{env}") - public Environment master(@PathVariable String name, @PathVariable String env) { - return properties(name, env, "master"); + @RequestMapping("/{name}/{profiles:.*[^-].*}") + public Environment master(@PathVariable String name, @PathVariable String profiles) { + return labelled(name, profiles, "master"); } - @RequestMapping("/{name}/{env}/{label}") - public Environment properties(@PathVariable String name, @PathVariable String env, @PathVariable String label) { - return encryption.decrypt(repository.findOne(name, env, label)); + @RequestMapping("/{name}/{profiles}/{label}") + public Environment labelled(@PathVariable String name, @PathVariable String profiles, + @PathVariable String label) { + return encryption.decrypt(repository.findOne(name, profiles, label)); + } + + @RequestMapping("/{name}-{profiles}.properties") + public ResponseEntity properties(@PathVariable String name, + @PathVariable String profiles) throws IOException { + return labelledProperties(name, profiles, "master"); + } + + @RequestMapping("/{label}/{name}-{profiles}.properties") + public ResponseEntity labelledProperties(@PathVariable String name, + @PathVariable String profiles, @PathVariable String label) throws IOException { + if (name.contains("-") || profiles.contains("-")) { + throw new IllegalArgumentException("Properties output not supported for name or profiles containing hyphens"); + } + Properties properties = convertToProperties(labelled(name, profiles, label)); + return getSuccess(sortLines(properties)); + } + + private String sortLines(Properties properties) throws IOException { + List list = new ArrayList(); + for (Entry entry : properties.entrySet()) { + if (entry.getKey().equals("spring.profiles")) { + continue; + } + String line = entry.getKey() + ": " + entry.getValue(); + list.add(line); + } + Collections.sort(list); + StringBuilder output = new StringBuilder(); + for (String item : list) { + if (output.length() > 0) { + output.append("\n"); + } + output.append(item); + } + return output.toString(); + } + + @RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" }) + public ResponseEntity yaml(@PathVariable String name, @PathVariable String profiles) + throws Exception { + return labelledYaml(name, profiles, "master"); + } + + @RequestMapping({ "/{label}/{name}-{profiles}.yml", "/{label}/{name}-{profiles}.yaml" }) + public ResponseEntity labelledYaml(@PathVariable String name, + @PathVariable String profiles, @PathVariable String label) throws Exception { + if (name.contains("-") || profiles.contains("-")) { + throw new IllegalArgumentException("YAML output not supported for name or profiles containing hyphens"); + } + LinkedHashMap target = new LinkedHashMap(); + PropertiesConfigurationFactory> factory = new PropertiesConfigurationFactory>( + target); + Properties properties = convertToProperties(labelled(name, profiles, label)); + addArrays(target, properties); + factory.setProperties(properties); + factory.bindPropertiesToTarget(); + Map input = factory.getObject(); + return getSuccess(new Yaml().dumpAsMap(input)); + } + + @ExceptionHandler(IllegalArgumentException.class) + public void illegalArgument(HttpServletResponse response) throws IOException { + response.sendError(HttpStatus.BAD_REQUEST.value()); + } + + private ResponseEntity getSuccess(String body) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + return new ResponseEntity(body, headers, HttpStatus.OK); + } + + /** + * Create Lists of the right size for any YAML arrays that are going to need to be + * bound. Some of this might be do-able in RelaxedDataBinder, but we need to do it + * here for now. Only supports arrays at leaf level currently (i.e. the properties + * keys end in [*]). + * + * @param target the target Map + * @param properties the properties (with key names to check) + */ + private void addArrays(LinkedHashMap target, Properties properties) { + for (String key : properties.stringPropertyNames()) { + int index = key.indexOf("["); + Map current = target; + if (index > 0) { + String stem = key.substring(0, index); + String[] keys = StringUtils.split(stem, "."); + for (int i = 0; i < keys.length - 1; i++) { + if (current.get(keys[i]) == null) { + LinkedHashMap map = new LinkedHashMap(); + current.put(keys[i], map); + current = map; + } + else { + @SuppressWarnings("unchecked") + Map map = (Map) current + .get(keys[i]); + current = map; + } + } + String name = keys[keys.length - 1]; + if (current.get(name) == null) { + current.put(name, new ArrayList()); + } + @SuppressWarnings("unchecked") + List value = (List) current.get(name); + int position = Integer + .valueOf(key.substring(index + 1, key.indexOf("]"))); + while (position >= value.size()) { + value.add(""); + } + } + } + } + + private Properties convertToProperties(Environment profiles) { + Properties map = new Properties(); + for (PropertySource source : profiles.getPropertySources()) { + @SuppressWarnings("unchecked") + Map value = (Map) source.getSource(); + map.putAll(value); + } + for (Entry entry : map.entrySet()) { + map.put(entry.getKey(), entry.getValue().toString()); + } + return map; } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java new file mode 100644 index 00000000..23bc74e7 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2013-2014 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.config.server; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.springframework.cloud.config.Environment; +import org.springframework.cloud.config.PropertySource; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * @author Dave Syer + * + */ +public class EnvironmentControllerTests { + + @Rule + public ExpectedException expected = ExpectedException.none(); + + private EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class); + + private EnvironmentController controller = new EnvironmentController(repository, + new EncryptionController()); + + private Environment environment = new Environment("foo", "master"); + + @Test + public void vanillaYaml() throws Exception { + Map map = new HashMap(); + map.put("a.b.c", "d"); + environment.add(new PropertySource("one", map)); + Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); + String yaml = controller.yaml("foo", "bar").getBody(); + assertEquals("a:\n b:\n c: d\n", yaml); + } + + @Test + public void arrayInYaml() throws Exception { + Map map = new LinkedHashMap(); + map.put("a.b[0]", "c"); + map.put("a.b[1]", "d"); + environment.add(new PropertySource("one", map)); + Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); + String yaml = controller.yaml("foo", "bar").getBody(); + assertEquals("a:\n b:\n - c\n - d\n", yaml); + } + + @Test + public void mappingForEnvironment() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/foo/bar")).andExpect( + MockMvcResultMatchers.status().isOk()); + } + + @Test + public void mappingForLabelledEnvironment() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/foo/bar/other")).andExpect( + MockMvcResultMatchers.status().isOk()); + } + + @Test + public void mappingForYaml() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/foo-bar.yml")).andExpect( + MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); + } + + @Test + public void mappingForLabelledYaml() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar.yml")).andExpect( + MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); + } + + @Test + public void mappingForLabelledProperties() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "other")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar.properties")).andExpect( + MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); + } + + @Test + public void mappingForProperties() throws Exception { + Mockito.when(repository.findOne("foo", "bar", "master")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/foo-bar.properties")).andExpect( + MockMvcResultMatchers.content().contentType(MediaType.TEXT_PLAIN)); + } + + @Test + public void mappingForLabelledYamlWithHyphen() throws Exception { + Mockito.when(repository.findOne("foo", "bar-spam", "other")).thenReturn(environment); + MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build(); + mvc.perform(MockMvcRequestBuilders.get("/other/foo-bar-spam.yml")).andExpect( + MockMvcResultMatchers.status().isBadRequest()); + } +}