diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index c30d8e87..1a6c1b5d 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -88,7 +88,12 @@ filesystem repository, so that the server can clone it and use a local working copy as a cache. This repository implementation maps the `{label}` parameter of the -HTTP resource to a git label (commit id, branch name or tag). +HTTP resource to a git label (commit id, branch name or tag). If the +git branch or tag name contains a slash ("/") then the label in the +HTTP URL should be specified with the special string "(_)" instead (to +avoid ambiguity with other URL paths). Be careful with the brackets in +the URL if you are using a command line client like curl (e.g. escape +them from the shell with quotes ''). Spring Cloud Config Server supports a single or multiple git repositories: 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 ff1150cd..dd6774e7 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 @@ -92,18 +92,25 @@ public class EnvironmentController { @RequestMapping("/{name}/{profiles:.*[^-].*}") public Environment defaultLabel(@PathVariable String name, @PathVariable String profiles) { - return labelled(name, profiles, defaultLabel); + return labelled(name, profiles, this.defaultLabel); } @RequestMapping("/{name}/{profiles}/{label:.*}") public Environment labelled(@PathVariable String name, @PathVariable String profiles, @PathVariable String label) { - Environment environment = repository.findOne(name, profiles, label); - if (environmentEncryptor != null) { - environment = environmentEncryptor.decrypt(environment); + if (label==null) { + label = this.defaultLabel; } - if (!overrides.isEmpty()) { - environment.addFirst(new PropertySource("overrides", overrides)); + if (label!=null && label.contains("(_)")) { + // "(_)" is uncommon in a git branch name, but "/" cannot be matched by Spring MVC + label = label.replace("(_)", "/"); + } + Environment environment = this.repository.findOne(name, profiles, label); + if (this.environmentEncryptor != null) { + environment = this.environmentEncryptor.decrypt(environment); + } + if (!this.overrides.isEmpty()) { + environment.addFirst(new PropertySource("overrides", this.overrides)); } return environment; } @@ -111,7 +118,7 @@ public class EnvironmentController { @RequestMapping("/{name}-{profiles}.properties") public ResponseEntity properties(@PathVariable String name, @PathVariable String profiles) throws IOException { - return labelledProperties(name, profiles, defaultLabel); + return labelledProperties(name, profiles, this.defaultLabel); } @RequestMapping("/{label}/{name}-{profiles}.properties") @@ -126,7 +133,7 @@ public class EnvironmentController { @RequestMapping("{name}-{profiles}.json") public ResponseEntity> jsonProperties(@PathVariable String name, @PathVariable String profiles) throws Exception { - return labelledJsonProperties(name, profiles, defaultLabel); + return labelledJsonProperties(name, profiles, this.defaultLabel); } @RequestMapping("/{label}/{name}-{profiles}.json") @@ -153,7 +160,7 @@ public class EnvironmentController { @RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" }) public ResponseEntity yaml(@PathVariable String name, @PathVariable String profiles) throws Exception { - return labelledYaml(name, profiles, defaultLabel); + return labelledYaml(name, profiles, this.defaultLabel); } @RequestMapping({ "/{label}/{name}-{profiles}.yml", "/{label}/{name}-{profiles}.yaml" }) @@ -251,7 +258,7 @@ public class EnvironmentController { else { @SuppressWarnings("unchecked") Map map = (Map) current - .get(keys[i]); + .get(keys[i]); current = map; } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java index 15e09ccc..6d5f84d6 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/EnvironmentControllerIntegrationTests.java @@ -16,31 +16,48 @@ package org.springframework.cloud.config.server; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.EnvironmentControllerIntegrationTests.ControllerConfiguration; import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; 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; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * @author Dave Syer * @author Roy Clarkson */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = ControllerConfiguration.class) +@WebAppConfiguration public class EnvironmentControllerIntegrationTests { + @Autowired + private WebApplicationContext context; private MockMvc mvc; - private EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class);; + @Autowired + private EnvironmentRepository repository; @Before public void init() { - Mockito.when(this.repository.getDefaultLabel()).thenReturn("master"); - this.mvc = MockMvcBuilders.standaloneSetup( - new EnvironmentController(this.repository, - new CipherEnvironmentEncryptor(null))).build(); + Mockito.reset(this.repository); + this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); } @Test @@ -49,6 +66,34 @@ public class EnvironmentControllerIntegrationTests { new Environment("foo", "default")); this.mvc.perform(MockMvcRequestBuilders.get("/foo/default")).andExpect( MockMvcResultMatchers.status().isOk()); + Mockito.verify(this.repository).findOne("foo", "default", "master"); + } + + @Test + public void propertiesNoLabel() throws Exception { + Mockito.when(this.repository.findOne("foo", "default", "master")).thenReturn( + new Environment("foo", "default")); + this.mvc.perform(MockMvcRequestBuilders.get("/foo-default.properties")).andExpect( + MockMvcResultMatchers.status().isOk()); + Mockito.verify(this.repository).findOne("foo", "default", "master"); + } + + @Test + public void propertiesLabel() throws Exception { + Mockito.when(this.repository.findOne("foo", "default", "label")).thenReturn( + new Environment("foo", "default")); + this.mvc.perform(MockMvcRequestBuilders.get("/label/foo-default.properties")).andExpect( + MockMvcResultMatchers.status().isOk()); + Mockito.verify(this.repository).findOne("foo", "default", "label"); + } + + @Test + public void propertiesLabelWithSlash() throws Exception { + Mockito.when(this.repository.findOne("foo", "default", "label/spam")).thenReturn( + new Environment("foo", "default")); + this.mvc.perform(MockMvcRequestBuilders.get("/label(_)spam/foo-default.properties")).andExpect( + MockMvcResultMatchers.status().isOk()); + Mockito.verify(this.repository).findOne("foo", "default", "label/spam"); } @Test @@ -67,4 +112,33 @@ public class EnvironmentControllerIntegrationTests { MockMvcResultMatchers.status().isOk()); } + @Test + public void environmentWithLabelContainingSlash() throws Exception { + Mockito.when(this.repository.findOne("foo", "default", "feature/puff")) + .thenReturn(new Environment("foo", "default")); + this.mvc.perform(MockMvcRequestBuilders.get("/foo/default/feature(_)puff")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("\"propertySources\":"))); + } + + @Configuration + @EnableWebMvc + @Import(PropertyPlaceholderAutoConfiguration.class) + public static class ControllerConfiguration { + + @Bean + public EnvironmentRepository environmentRepository() { + EnvironmentRepository repository = Mockito.mock(EnvironmentRepository.class); + Mockito.when(repository.getDefaultLabel()).thenReturn("master"); + return repository; + } + + @Bean + public EnvironmentController controller() { + return new EnvironmentController(environmentRepository(), + new CipherEnvironmentEncryptor(null)); + } + + } + }