[bs-93] Add /info endpoint with git properties etc.

* If git.properties is on the classpath (e.g. from the Maven plugin)
/info will list the commit id, branch and dates.
* If application.yml has an info object at the top level that will
be diplayed as well (so e.g. you can use Maven resource filtering
top add the project name, version etc.)
* RelaxedDataBinder can now be used to bind to a Map (as opposed to
a nested Map inside teh target bean)

[Fixes #49130073]
pull/1/merge
Dave Syer 12 years ago
parent 7e6651c0a2
commit 7e548b5bd4

@ -10,7 +10,6 @@
<artifactId>spring-bootstrap-applications</artifactId>
<packaging>pom</packaging>
<properties>
<main.basedir>${project.basedir}/..</main.basedir>
<spring.bootstrap.version>0.0.1-SNAPSHOT</spring.bootstrap.version>
<start-class>org.springframework.bootstrap.main.Spring</start-class>
</properties>

@ -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<String, Object> infoMap;
@Autowired
@Qualifier("gitInfo")
private GitInfo gitInfo;
@Bean
public Map<String, Object> applicationInfo() {
LinkedHashMap<String, Object> info = new LinkedHashMap<String, Object>();
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> gitInfo() throws IOException {
PropertiesConfigurationFactory<GitInfo> factory = new PropertiesConfigurationFactory<GitInfo>(
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<Map<String, Object>> infoMap() {
PropertiesConfigurationFactory<Map<String, Object>> factory = new PropertiesConfigurationFactory<Map<String, Object>>(
new LinkedHashMap<String, Object>());
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;
}
}
}
}

@ -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

@ -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;

@ -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<String, Object> info;
/**
* @param info
*/
public InfoEndpoint(Map<String, Object> info) {
this.info = new LinkedHashMap<String, Object>(info);
this.info.putAll(getAdditionalInfo());
}
@RequestMapping("${endpoints.info.path:/info}")
@ResponseBody
public Map<String, Object> info() {
return this.info;
}
protected Map<String, Object> getAdditionalInfo() {
return Collections.emptyMap();
}
}

@ -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;
}

@ -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<String, Object> map = (Map<String, Object>) 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<PropertyValue> 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<String, Object> nested = new LinkedHashMap<String, Object>();
if (target.getPropertyValue(base) != null) {
@SuppressWarnings("unchecked")
@ -136,24 +156,33 @@ public class RelaxedDataBinder extends DataBinder {
} else {
target.setPropertyValue(base, nested);
}
Map<String, Object> value = nested;
nested = new LinkedHashMap<String, Object>();
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, Object>();
}
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<String, Object> target,
MutablePropertyValues propertyValues, int index, String base) {
PropertyValue propertyValue = propertyValues.getPropertyValueList().get(index);
String name = propertyValue.getName();
String suffix = name.substring(base.length());
Map<String, Object> value = new LinkedHashMap<String, Object>();
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, Object>();
}
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<String, Object> map;
public MapHolder(Map<String, Object> map) {
this.map = map;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
public Map<String, Object> getMap() {
return this.map;
}
}
}

@ -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<String, Object> target = new LinkedHashMap<String, Object>();
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<String, Object> target = new LinkedHashMap<String, Object>();
BindingResult result = bind(target, "spam: bar\n" + "vanilla.foo.value: 123",
"vanilla");
assertEquals(0, result.getErrorCount());
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) target.get("foo");
assertEquals("123", map.get("value"));
}
private BindingResult bind(Object target, String values) throws Exception {
return bind(target, values, null);
}

Loading…
Cancel
Save