diff --git a/spring-bootstrap-applications/pom.xml b/spring-bootstrap-applications/pom.xml index 315bb3f163..ef63a878ad 100644 --- a/spring-bootstrap-applications/pom.xml +++ b/spring-bootstrap-applications/pom.xml @@ -10,7 +10,6 @@ spring-bootstrap-applications pom - ${project.basedir}/.. 0.0.1-SNAPSHOT org.springframework.bootstrap.main.Spring diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/InfoConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/InfoConfiguration.java new file mode 100644 index 0000000000..015cc292f2 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/InfoConfiguration.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2013 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.bootstrap.autoconfigure.service; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.Resource; +import javax.servlet.Servlet; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.bootstrap.bind.PropertiesConfigurationFactory; +import org.springframework.bootstrap.context.annotation.ConditionalOnClass; +import org.springframework.bootstrap.context.annotation.ConditionalOnMissingBean; +import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration; +import org.springframework.bootstrap.service.info.InfoEndpoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for /info endpoint. + * + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) +@ConditionalOnMissingBean({ InfoEndpoint.class }) +public class InfoConfiguration { + + @Resource(name = "infoMap") + private Map infoMap; + + @Autowired + @Qualifier("gitInfo") + private GitInfo gitInfo; + + @Bean + public Map applicationInfo() { + LinkedHashMap info = new LinkedHashMap(); + info.putAll(this.infoMap); + if (this.gitInfo.getBranch() != null) { + info.put("git", this.gitInfo); + } + return info; + } + + @Bean + public InfoEndpoint infoEndpoint() { + return new InfoEndpoint(applicationInfo()); + } + + @Configuration + public static class InfoPropertiesConfiguration { + + @Autowired + private ConfigurableEnvironment environment = new StandardEnvironment(); + + @Bean + public PropertiesConfigurationFactory gitInfo() throws IOException { + PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory( + new GitInfo()); + factory.setTargetName("git"); + Properties properties = new Properties(); + if (new ClassPathResource("git.properties").exists()) { + properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource( + "git.properties")); + } + factory.setProperties(properties); + return factory; + } + + @Bean + public PropertiesConfigurationFactory> infoMap() { + PropertiesConfigurationFactory> factory = new PropertiesConfigurationFactory>( + new LinkedHashMap()); + factory.setTargetName("info"); + factory.setPropertySources(this.environment.getPropertySources()); + return factory; + } + + } + + public static class GitInfo { + private String branch; + private Commit commit = new Commit(); + + public String getBranch() { + return this.branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public Commit getCommit() { + return this.commit; + } + + public static class Commit { + private String id; + private String time; + + public String getId() { + return this.id == null ? "" : (this.id.length() > 7 ? this.id.substring( + 0, 7) : this.id); + } + + public void setId(String id) { + this.id = id; + } + + public String getTime() { + return this.time; + } + + public void setTime(String time) { + this.time = time; + } + } + } +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/SecurityConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/SecurityConfiguration.java index d968630d4c..806d606e50 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/SecurityConfiguration.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/SecurityConfiguration.java @@ -61,6 +61,9 @@ public class SecurityConfiguration { @Value("${endpoints.healthz.path:/healthz}") private String healthzPath = "/healthz"; + @Value("${endpoints.info.path:/info}") + private String infoPath = "/info"; + @Autowired private SecurityProperties security; @@ -70,6 +73,7 @@ public class SecurityConfiguration { @Override protected void ignoredRequests(IgnoredRequestRegistry ignoredRequests) { ignoredRequests.antMatchers(this.healthzPath); + ignoredRequests.antMatchers(this.infoPath); } @Override diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServerConfiguration.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServerConfiguration.java index a8524f1eda..9c5399af5d 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServerConfiguration.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/autoconfigure/service/ServerConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.bootstrap.service.properties.ServerProperties; import org.springframework.bootstrap.service.properties.ServerProperties.Tomcat; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.Order; import org.springframework.util.StringUtils; @@ -50,6 +51,7 @@ import org.springframework.util.StringUtils; @Configuration @ConditionalOnClass({ Servlet.class }) @Order(Integer.MIN_VALUE) +@Import(InfoConfiguration.class) public class ServerConfiguration implements BeanPostProcessor, BeanFactoryAware { private BeanFactory beanFactory; diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/info/InfoEndpoint.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/info/InfoEndpoint.java new file mode 100644 index 0000000000..7e80af4a85 --- /dev/null +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/info/InfoEndpoint.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2013 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.bootstrap.service.info; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Dave Syer + */ +@Controller +public class InfoEndpoint { + + private Map info; + + /** + * @param info + */ + public InfoEndpoint(Map info) { + this.info = new LinkedHashMap(info); + this.info.putAll(getAdditionalInfo()); + } + + @RequestMapping("${endpoints.info.path:/info}") + @ResponseBody + public Map info() { + return this.info; + } + + protected Map getAdditionalInfo() { + return Collections.emptyMap(); + } + +} diff --git a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/EndpointsProperties.java b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/EndpointsProperties.java index 608cc8f3a2..3a4ef83bca 100644 --- a/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/EndpointsProperties.java +++ b/spring-bootstrap-service/src/main/java/org/springframework/bootstrap/service/properties/EndpointsProperties.java @@ -30,6 +30,9 @@ import org.springframework.bootstrap.context.annotation.ConfigurationProperties; @ConfigurationProperties(name = "endpoints", ignoreUnknownFields = false) public class EndpointsProperties { + @Valid + private Endpoint info = new Endpoint("/info"); + @Valid private Endpoint varz = new Endpoint("/varz"); @@ -48,6 +51,10 @@ public class EndpointsProperties { @Valid private Endpoint dump = new Endpoint("/dump"); + public Endpoint getInfo() { + return this.info; + } + public Endpoint getVarz() { return this.varz; } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/RelaxedDataBinder.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/RelaxedDataBinder.java index 26e3f30078..f11e83c424 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/RelaxedDataBinder.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/RelaxedDataBinder.java @@ -43,7 +43,16 @@ public class RelaxedDataBinder extends DataBinder { * @param target the target into which properties are bound */ public RelaxedDataBinder(Object target) { - super(target); + super(wrapTarget(target)); + } + + private static Object wrapTarget(Object target) { + if (target instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) target; + target = new MapHolder(map); + } + return target; } /** @@ -51,7 +60,7 @@ public class RelaxedDataBinder extends DataBinder { * @param namePrefix An optional prefix to be used when reading properties */ public RelaxedDataBinder(Object target, String namePrefix) { - super(target, (StringUtils.hasLength(namePrefix) ? namePrefix + super(wrapTarget(target), (StringUtils.hasLength(namePrefix) ? namePrefix : DEFAULT_OBJECT_NAME)); this.namePrefix = (StringUtils.hasLength(namePrefix) ? namePrefix + "." : null); } @@ -74,11 +83,15 @@ public class RelaxedDataBinder extends DataBinder { private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues, Object target) { + propertyValues = getProperyValuesForNamePrefix(propertyValues); + + if (target instanceof MapHolder) { + propertyValues = addMapPrefix(propertyValues); + } + BeanWrapper targetWrapper = new BeanWrapperImpl(target); targetWrapper.setAutoGrowNestedPaths(true); - propertyValues = getProperyValuesForNamePrefix(propertyValues); - List list = propertyValues.getPropertyValueList(); for (int i = 0; i < list.size(); i++) { modifyProperty(propertyValues, targetWrapper, list.get(i), i); @@ -86,6 +99,14 @@ public class RelaxedDataBinder extends DataBinder { return propertyValues; } + private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) { + MutablePropertyValues rtn = new MutablePropertyValues(); + for (PropertyValue pv : propertyValues.getPropertyValues()) { + rtn.add("map." + pv.getName(), pv.getValue()); + } + return rtn; + } + private MutablePropertyValues getProperyValuesForNamePrefix( MutablePropertyValues propertyValues) { if (this.namePrefix == null) { @@ -126,7 +147,6 @@ public class RelaxedDataBinder extends DataBinder { // Any nested properties that are maps, are assumed to be simple nested // maps of maps... if (type != null && Map.class.isAssignableFrom(type)) { - String suffix = name.substring(base.length()); Map nested = new LinkedHashMap(); if (target.getPropertyValue(base) != null) { @SuppressWarnings("unchecked") @@ -136,24 +156,33 @@ public class RelaxedDataBinder extends DataBinder { } else { target.setPropertyValue(base, nested); } - Map value = nested; - nested = new LinkedHashMap(); - String[] tree = StringUtils.delimitedListToStringArray(suffix, "."); - for (int j = 1; j < tree.length - 1; j++) { - if (!value.containsKey(tree[j])) { - value.put(tree[j], nested); - } - value = nested; - nested = new LinkedHashMap(); - } - String refName = base + suffix.replaceAll("\\.([a-zA-Z0-9]*)", "[$1]"); - propertyValues.setPropertyValueAt(new PropertyValue(refName, - propertyValue.getValue()), index); + modifyPopertiesForMap(nested, propertyValues, index, base); break; } } } + private void modifyPopertiesForMap(Map target, + MutablePropertyValues propertyValues, int index, String base) { + PropertyValue propertyValue = propertyValues.getPropertyValueList().get(index); + String name = propertyValue.getName(); + String suffix = name.substring(base.length()); + Map value = new LinkedHashMap(); + String[] tree = StringUtils.delimitedListToStringArray( + suffix.startsWith(".") ? suffix.substring(1) : suffix, "."); + for (int j = 0; j < tree.length - 1; j++) { + if (!target.containsKey(tree[j])) { + target.put(tree[j], value); + } + target = value; + value = new LinkedHashMap(); + } + String refName = base + suffix.replaceAll("\\.([a-zA-Z0-9]*)", "[$1]"); + propertyValues.setPropertyValueAt( + new PropertyValue(refName, propertyValue.getValue()), index); + + } + private String getActualPropertyName(BeanWrapper target, String prefix, String name) { for (Variation variation : Variation.values()) { for (Manipulation manipulation : Manipulation.values()) { @@ -223,4 +252,20 @@ public class RelaxedDataBinder extends DataBinder { public abstract String apply(String value); } + static class MapHolder { + private Map map; + + public MapHolder(Map map) { + this.map = map; + } + + public void setMap(Map map) { + this.map = map; + } + + public Map getMap() { + return this.map; + } + } + } diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java index 887f031e7d..6bb3020e42 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java @@ -17,6 +17,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -181,6 +182,26 @@ public class RelaxedDataBinderTests { assertEquals(123, target.getValue()); } + @Test + public void testBindMap() throws Exception { + Map target = new LinkedHashMap(); + BindingResult result = bind(target, "spam: bar\n" + "vanilla.value: 123", + "vanilla"); + assertEquals(0, result.getErrorCount()); + assertEquals("123", target.get("value")); + } + + @Test + public void testBindMapNestedInMap() throws Exception { + Map target = new LinkedHashMap(); + BindingResult result = bind(target, "spam: bar\n" + "vanilla.foo.value: 123", + "vanilla"); + assertEquals(0, result.getErrorCount()); + @SuppressWarnings("unchecked") + Map map = (Map) target.get("foo"); + assertEquals("123", map.get("value")); + } + private BindingResult bind(Object target, String values) throws Exception { return bind(target, values, null); }