From b906b186556b663a6201914cb965c0e8a47dd276 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 8 Mar 2016 14:36:57 +0100 Subject: [PATCH] Initiate GitProperties This commit polish the new info contributor infrastructure by migrating `GitInfo` to `GitProperties`. `InfoProperties` provides an abstraction that exposes unstructured data in an immutable way. The `GitInfoContributor` now accepts a "mode" that determines if all data should be exposed or only a sub-set of known keys. Closes gh-2644 --- .../InfoContributorAutoConfiguration.java | 22 ++- .../InfoContributorProperties.java | 54 ++++++ .../info/EnvironmentInfoContributor.java | 13 +- .../boot/actuate/info/GitInfoContributor.java | 164 ++++++++++++++++++ ...ibutor.java => PropertySourcesBinder.java} | 37 ++-- ...InfoContributorAutoConfigurationTests.java | 64 ++++++- .../actuate/info/GitInfoContributorTests.java | 64 +++++++ .../boot/autoconfigure/info/GitInfo.java | 74 -------- .../info/ProjectInfoAutoConfiguration.java | 86 ++------- .../ProjectInfoAutoConfigurationTests.java | 42 ++--- .../appendix-application-properties.adoc | 1 + .../boot/info/GitProperties.java | 116 +++++++++++++ .../boot/info/InfoProperties.java | 128 ++++++++++++++ .../boot/info/GitPropertiesTests.java | 87 ++++++++++ .../boot/info/InfoPropertiesTests.java | 91 ++++++++++ 15 files changed, 841 insertions(+), 202 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorProperties.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/GitInfoContributor.java rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/{AbstractEnvironmentInfoContributor.java => PropertySourcesBinder.java} (65%) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/GitInfoContributorTests.java delete mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/GitInfo.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/info/GitProperties.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/info/InfoProperties.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/info/GitPropertiesTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/info/InfoPropertiesTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfiguration.java index 3c6fb20e15..2bf3ffc1a0 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfiguration.java @@ -16,17 +16,17 @@ package org.springframework.boot.actuate.autoconfigure; -import java.io.IOException; - import org.springframework.boot.actuate.info.EnvironmentInfoContributor; +import org.springframework.boot.actuate.info.GitInfoContributor; import org.springframework.boot.actuate.info.InfoContributor; -import org.springframework.boot.actuate.info.SimpleInfoContributor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.boot.autoconfigure.info.GitInfo; import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.GitProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -44,6 +44,7 @@ import org.springframework.core.env.ConfigurableEnvironment; @Configuration @AutoConfigureAfter(ProjectInfoAutoConfiguration.class) @AutoConfigureBefore(EndpointAutoConfiguration.class) +@EnableConfigurationProperties(InfoContributorProperties.class) public class InfoContributorAutoConfiguration { /** @@ -51,6 +52,12 @@ public class InfoContributorAutoConfiguration { */ public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10; + private final InfoContributorProperties properties; + + public InfoContributorAutoConfiguration(InfoContributorProperties properties) { + this.properties = properties; + } + @Bean @ConditionalOnEnabledInfoContributor("env") @Order(DEFAULT_ORDER) @@ -61,10 +68,11 @@ public class InfoContributorAutoConfiguration { @Bean @ConditionalOnEnabledInfoContributor("git") - @ConditionalOnSingleCandidate(GitInfo.class) + @ConditionalOnSingleCandidate(GitProperties.class) + @ConditionalOnMissingBean @Order(DEFAULT_ORDER) - public InfoContributor gitInfoContributor(GitInfo gitInfo) throws IOException { - return new SimpleInfoContributor("git", gitInfo); + public GitInfoContributor gitInfoContributor(GitProperties gitProperties) { + return new GitInfoContributor(gitProperties, this.properties.getGit().getMode()); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorProperties.java new file mode 100644 index 0000000000..26a2ecc694 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/InfoContributorProperties.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2016 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.boot.actuate.autoconfigure; + +import org.springframework.boot.actuate.info.GitInfoContributor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for core info contributors. + * + * @author Stephane Nicoll + * @since 1.4.0 + */ +@ConfigurationProperties("management.info") +public class InfoContributorProperties { + + private final Git git = new Git(); + + public Git getGit() { + return this.git; + } + + public static class Git { + + /** + * Mode to use to expose git information. + */ + private GitInfoContributor.Mode mode = GitInfoContributor.Mode.SIMPLE; + + public GitInfoContributor.Mode getMode() { + return this.mode; + } + + public void setMode(GitInfoContributor.Mode mode) { + this.mode = mode; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java index 956018f08c..adefd96fe4 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/EnvironmentInfoContributor.java @@ -16,29 +16,26 @@ package org.springframework.boot.actuate.info; -import java.util.Map; - import org.springframework.core.env.ConfigurableEnvironment; /** - * A {@link InfoContributor} that provides all environment entries prefixed with info. + * An {@link InfoContributor} that provides all environment entries prefixed with info. * * @author Meang Akira Tanaka * @author Stephane Nicoll * @since 1.4.0 */ -public class EnvironmentInfoContributor extends AbstractEnvironmentInfoContributor { +public class EnvironmentInfoContributor implements InfoContributor { - private final Map info; + private final PropertySourcesBinder binder; public EnvironmentInfoContributor(ConfigurableEnvironment environment) { - super(environment); - this.info = extract("info"); + this.binder = new PropertySourcesBinder(environment); } @Override public void contribute(Info.Builder builder) { - builder.withDetails(this.info); + builder.withDetails(this.binder.extractAll("info")); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/GitInfoContributor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/GitInfoContributor.java new file mode 100644 index 0000000000..c798833c6a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/GitInfoContributor.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-2016 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.boot.actuate.info; + +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.info.GitProperties; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +/** + * An {@link InfoContributor} that exposes {@link GitProperties}. + * + * @author Stephane Nicoll + * @since 1.4.0 + */ +public class GitInfoContributor implements InfoContributor { + + private static final Log logger = LogFactory.getLog(GitInfoContributor.class); + + private final GitProperties properties; + + private final Mode mode; + + public GitInfoContributor(GitProperties properties, Mode mode) { + this.properties = properties; + this.mode = mode; + } + + public GitInfoContributor(GitProperties properties) { + this(properties, Mode.SIMPLE); + } + + protected final GitProperties getProperties() { + return this.properties; + } + + protected final Mode getMode() { + return this.mode; + } + + @Override + public void contribute(Info.Builder builder) { + builder.withDetail("git", extractContent()); + } + + /** + * Extract the content to contribute to the info endpoint. + * @return the content to expose + */ + protected Map extractContent() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(toPropertySources()); + Map content = new PropertySourcesBinder(propertySources).extractAll(""); + postProcess(content); + return content; + } + + /** + * Post-process the content to expose. By default, well known keys representing dates + * are converted to {@link Date} instances. + * @param content the content to expose + */ + protected void postProcess(Map content) { + coerceDate(getNestedMap(content, "commit"), "time"); + coerceDate(getNestedMap(content, "build"), "time"); + } + + /** + * Coerce the specified key if the value is a proper epoch time. + * @param content the content to expose + * @param key the property to coerce + */ + protected void coerceDate(Map content, String key) { + Object value = content.get(key); + if (value != null) { + try { + long epoch = Long.parseLong(value.toString()); + content.put(key, new Date(epoch)); + } + catch (NumberFormatException ex) { + logger.warn("Expected a date for '" + key + "'", ex); + } + } + } + + /** + * Return the {@link PropertySource} to use. + * @return the property source + */ + protected PropertySource toPropertySources() { + if (this.mode.equals(Mode.FULL)) { + return this.properties.toPropertySource(); + } + else { + Properties props = new Properties(); + copyIfSet(this.properties, props, "branch"); + String commitId = this.properties.getShortCommitId(); + if (commitId != null) { + props.put("commit.id", commitId); + } + copyIfSet(this.properties, props, "commit.time"); + return new PropertiesPropertySource("git", props); + } + } + + private void copyIfSet(GitProperties source, Properties target, String key) { + String value = source.get(key); + if (StringUtils.hasText(value)) { + target.put(key, value); + } + } + + @SuppressWarnings("unchecked") + private Map getNestedMap(Map map, String key) { + Object o = map.get(key); + if (o == null) { + return Collections.emptyMap(); + } + else { + return (Map) o; + } + } + + /** + * Defines how git properties should be exposed. + */ + public enum Mode { + + /** + * Expose all available data, including custom properties. + */ + FULL, + + /** + * Expose a pre-defined set of core settings only. + */ + SIMPLE + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/AbstractEnvironmentInfoContributor.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/PropertySourcesBinder.java similarity index 65% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/AbstractEnvironmentInfoContributor.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/PropertySourcesBinder.java index 8329274b35..76db2cf9ba 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/AbstractEnvironmentInfoContributor.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/PropertySourcesBinder.java @@ -21,38 +21,43 @@ import java.util.Map; import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySources; +import org.springframework.util.StringUtils; import org.springframework.validation.BindException; /** - * A base {@link InfoContributor} implementation working on the {@link Environment}. + * Helper extracting info from {@link PropertySources}. * * @author Stephane Nicoll * @since 1.4.0 */ -public abstract class AbstractEnvironmentInfoContributor implements InfoContributor { +public class PropertySourcesBinder { - private final ConfigurableEnvironment environment; + private final PropertySources propertySources; - protected AbstractEnvironmentInfoContributor(ConfigurableEnvironment environment) { - this.environment = environment; + public PropertySourcesBinder(PropertySources propertySources) { + this.propertySources = propertySources; } - public final ConfigurableEnvironment getEnvironment() { - return this.environment; + public PropertySourcesBinder(ConfigurableEnvironment environment) { + this(environment.getPropertySources()); + } + + public final PropertySources getPropertySources() { + return this.propertySources; } /** - * Extract the keys from the environment using the specified {@code prefix}. The + * Extract the keys using the specified {@code prefix}. The * prefix won't be included. *

* Any key that starts with the {@code prefix} will be included * @param prefix the prefix to use - * @return the keys from the environment matching the prefix + * @return the keys matching the prefix */ - protected Map extract(String prefix) { + public Map extractAll(String prefix) { Map content = new LinkedHashMap(); - bindEnvironmentTo(prefix, content); + bindTo(prefix, content); return content; } @@ -63,11 +68,13 @@ public abstract class AbstractEnvironmentInfoContributor implements InfoContribu * @param prefix the prefix to use * @param target the object to bind to */ - protected void bindEnvironmentTo(String prefix, Object target) { + public void bindTo(String prefix, Object target) { PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory( target); - factory.setTargetName(prefix); - factory.setPropertySources(this.environment.getPropertySources()); + if (StringUtils.hasText(prefix)) { + factory.setTargetName(prefix); + } + factory.setPropertySources(this.propertySources); try { factory.bindPropertiesToTarget(); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfigurationTests.java index 417cd22a0f..cb88118d8b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/InfoContributorAutoConfigurationTests.java @@ -17,13 +17,15 @@ package org.springframework.boot.actuate.autoconfigure; import java.util.Map; +import java.util.Properties; import org.junit.After; import org.junit.Test; +import org.springframework.boot.actuate.info.GitInfoContributor; import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; -import org.springframework.boot.autoconfigure.info.GitInfo; +import org.springframework.boot.info.GitProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -74,12 +76,45 @@ public class InfoContributorAutoConfigurationTests { .isSameAs(beans.values().iterator().next()); } + @SuppressWarnings("unchecked") @Test - public void gitInfoAvailable() { - load(GitInfoConfiguration.class); + public void gitPropertiesDefaultMode() { + load(GitPropertiesConfiguration.class); Map beans = this.context .getBeansOfType(InfoContributor.class); assertThat(beans).containsKeys("gitInfoContributor"); + Map content = + invokeContributor(this.context.getBean("gitInfoContributor", InfoContributor.class)); + Object git = content.get("git"); + assertThat(git).isInstanceOf(Map.class); + Map gitInfo = (Map) git; + assertThat(gitInfo).containsOnlyKeys("branch", "commit"); + } + + @SuppressWarnings("unchecked") + @Test + public void gitPropertiesFullMode() { + load(GitPropertiesConfiguration.class, "management.info.git.mode=full"); + Map content = + invokeContributor(this.context.getBean("gitInfoContributor", InfoContributor.class)); + Object git = content.get("git"); + assertThat(git).isInstanceOf(Map.class); + Map gitInfo = (Map) git; + assertThat(gitInfo).containsOnlyKeys("branch", "commit", "foo"); + assertThat(gitInfo.get("foo")).isEqualTo("bar"); + } + + @Test + public void customGitInfoContributor() { + load(CustomGitInfoProviderConfiguration.class); + assertThat(this.context.getBean(GitInfoContributor.class)) + .isSameAs(this.context.getBean("customGitInfoContributor")); + } + + private Map invokeContributor(InfoContributor contributor) { + Info.Builder builder = new Info.Builder(); + contributor.contribute(builder); + return builder.build().getDetails(); } private void load(String... environment) { @@ -98,14 +133,15 @@ public class InfoContributorAutoConfigurationTests { } @Configuration - static class GitInfoConfiguration { + static class GitPropertiesConfiguration { @Bean - public GitInfo gitInfo() { - GitInfo gitInfo = new GitInfo(); - gitInfo.setBranch("master"); - gitInfo.getCommit().setId("abcdefg"); - return gitInfo; + public GitProperties gitProperties() { + Properties properties = new Properties(); + properties.put("branch", "master"); + properties.put("commit.id", "abcdefg"); + properties.put("foo", "bar"); + return new GitProperties(properties); } } @@ -124,4 +160,14 @@ public class InfoContributorAutoConfigurationTests { } + @Configuration + static class CustomGitInfoProviderConfiguration { + + @Bean + public GitInfoContributor customGitInfoContributor() { + return new GitInfoContributor(new GitProperties(new Properties())); + } + + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/GitInfoContributorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/GitInfoContributorTests.java new file mode 100644 index 0000000000..fc936dd597 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/GitInfoContributorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2016 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.boot.actuate.info; + +import java.util.Date; +import java.util.Map; +import java.util.Properties; + +import org.junit.Test; + +import org.springframework.boot.info.GitProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GitInfoContributor}. + * + * @author Stephane Nicoll + */ +public class GitInfoContributorTests { + + @SuppressWarnings("unchecked") + @Test + public void coerceDate() { + Properties properties = new Properties(); + properties.put("branch", "master"); + properties.put("commit.time", "2016-03-04T14:36:33+0100"); + GitInfoContributor contributor = new GitInfoContributor(new GitProperties(properties)); + Map content = contributor.extractContent(); + assertThat(content.get("commit")).isInstanceOf(Map.class); + Map commit = (Map) content.get("commit"); + Object commitTime = commit.get("time"); + assertThat(commitTime).isInstanceOf(Date.class); + assertThat(((Date) commitTime).getTime()).isEqualTo(1457098593000L); + } + + @SuppressWarnings("unchecked") + @Test + public void shortenCommitId() { + Properties properties = new Properties(); + properties.put("branch", "master"); + properties.put("commit.id", "8e29a0b0d423d2665c6ee5171947c101a5c15681"); + GitInfoContributor contributor = new GitInfoContributor(new GitProperties(properties)); + Map content = contributor.extractContent(); + assertThat(content.get("commit")).isInstanceOf(Map.class); + Map commit = (Map) content.get("commit"); + assertThat(commit.get("id")).isEqualTo("8e29a0b"); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/GitInfo.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/GitInfo.java deleted file mode 100644 index 8e8bfc1b40..0000000000 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/GitInfo.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2016 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.boot.autoconfigure.info; - -import java.util.Date; - -/** - * Provide git-related information such as commit id and time. - * - * @author Dave Syer - * @author Stephane Nicoll - * @since 1.4.0 - */ -public class GitInfo { - - private String branch; - - private final 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 Date time; - - public String getId() { - return (this.id == null ? "" : getShortId(this.id)); - } - - private String getShortId(String string) { - return string.substring(0, Math.min(this.id.length(), 7)); - } - - public void setId(String id) { - this.id = id; - } - - public Date getTime() { - return this.time; - } - - public void setTime(Date time) { - this.time = time; - } - - } - -} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java index 3e2a65794f..25be81b77d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java @@ -17,36 +17,25 @@ package org.springframework.boot.autoconfigure.info; import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; import java.util.Properties; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.bind.PropertiesConfigurationFactory; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.GitProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.ConversionFailedException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.PropertyResolver; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.validation.BindException; /** * {@link EnableAutoConfiguration Auto-configuration} for various project information. @@ -58,36 +47,31 @@ import org.springframework.validation.BindException; @EnableConfigurationProperties(ProjectInfoProperties.class) public class ProjectInfoAutoConfiguration { - @Autowired - private ProjectInfoProperties properties; + private final ProjectInfoProperties properties; - private final ConversionService conversionService = createConversionService(); + public ProjectInfoAutoConfiguration(ProjectInfoProperties properties) { + this.properties = properties; + } @Conditional(GitResourceAvailableCondition.class) @ConditionalOnMissingBean @Bean - public GitInfo gitInfo() throws Exception { - GitInfo gitInfo = new GitInfo(); - bindPropertiesTo(gitInfo, this.properties.getGit().getLocation(), "git"); - return gitInfo; + public GitProperties gitProperties() throws Exception { + return new GitProperties(loadFrom(this.properties.getGit().getLocation(), "git")); } - protected void bindPropertiesTo(Object target, Resource location, String prefix) - throws BindException, IOException { - PropertiesConfigurationFactory factory = new PropertiesConfigurationFactory( - target); - factory.setConversionService(this.conversionService); - factory.setTargetName(prefix); - Properties gitInfoProperties = PropertiesLoaderUtils.loadProperties(location); - factory.setProperties(gitInfoProperties); - factory.bindPropertiesToTarget(); + protected Properties loadFrom(Resource location, String prefix) throws IOException { + String p = prefix.endsWith(".") ? prefix : prefix + "."; + Properties source = PropertiesLoaderUtils.loadProperties(location); + Properties target = new Properties(); + for (String key : source.stringPropertyNames()) { + if (key.startsWith(p)) { + target.put(key.substring(p.length(), key.length()), source.get(key)); + } + } + return target; } - private static ConversionService createConversionService() { - DefaultConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter(new StringToDateConverter()); - return conversionService; - } static class GitResourceAvailableCondition extends SpringBootCondition { @@ -116,40 +100,4 @@ public class ProjectInfoAutoConfiguration { } - private static class StringToDateConverter implements Converter { - - @Override - public Date convert(String s) { - Long epoch = parseEpochSecond(s); - if (epoch != null) { - return new Date(epoch); - } - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", - Locale.US); - try { - return format.parse(s); - } - catch (ParseException ex) { - throw new ConversionFailedException(TypeDescriptor.valueOf(String.class), - TypeDescriptor.valueOf(Date.class), s, ex); - } - } - - /** - * Attempt to parse a {@code Long} from the specified input, representing the - * epoch time in seconds. - * @param s the input - * @return the epoch time in msec - */ - private Long parseEpochSecond(String s) { - try { - return Long.parseLong(s) * 1000; - } - catch (NumberFormatException ex) { - return null; - } - } - - } - } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java index 822d15ce77..45974c5621 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java @@ -17,11 +17,13 @@ package org.springframework.boot.autoconfigure.info; import java.util.Map; +import java.util.Properties; import org.junit.After; import org.junit.Test; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.info.GitProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -46,9 +48,9 @@ public class ProjectInfoAutoConfigurationTests { } @Test - public void gitInfoUnavailableIfResourceNotAvailable() { + public void gitPropertiesUnavailableIfResourceNotAvailable() { load(); - Map beans = this.context.getBeansOfType(GitInfo.class); + Map beans = this.context.getBeansOfType(GitProperties.class); assertThat(beans).hasSize(0); } @@ -56,34 +58,34 @@ public class ProjectInfoAutoConfigurationTests { public void gitLocationTakesPrecedenceOverLegacyKey() { load("spring.info.git.location=classpath:/org/springframework/boot/autoconfigure/info/git.properties", "spring.git.properties=classpath:/org/springframework/boot/autoconfigure/info/git-no-data.properties"); - GitInfo gitInfo = this.context.getBean(GitInfo.class); - assertThat(gitInfo.getBranch()).isNull(); - assertThat(gitInfo.getCommit().getId()).isEqualTo("f95038e"); - assertThat(gitInfo.getCommit().getTime().getTime()).isEqualTo(1456995720000L); + GitProperties gitProperties = this.context.getBean(GitProperties.class); + assertThat(gitProperties.getBranch()).isNull(); + assertThat(gitProperties.getCommitId()).isEqualTo("f95038ec09e29d8f91982fd1cbcc0f3b131b1d0a"); + assertThat(gitProperties.getCommitTime()).isEqualTo("1456995720000"); } @Test public void gitLegacyKeyIsUsedAsFallback() { load("spring.git.properties=classpath:/org/springframework/boot/autoconfigure/info/git-epoch.properties"); - GitInfo gitInfo = this.context.getBean(GitInfo.class); - assertThat(gitInfo.getBranch()).isEqualTo("master"); - assertThat(gitInfo.getCommit().getId()).isEqualTo("5009933"); - assertThat(gitInfo.getCommit().getTime().getTime()).isEqualTo(1457103850000L); + GitProperties gitProperties = this.context.getBean(GitProperties.class); + assertThat(gitProperties.getBranch()).isEqualTo("master"); + assertThat(gitProperties.getCommitId()).isEqualTo("5009933788f5f8c687719de6a697074ff80b1b69"); + assertThat(gitProperties.getCommitTime()).isEqualTo("1457103850000"); } @Test - public void gitInfoWithNoData() { + public void gitPropertiesWithNoData() { load("spring.info.git.location=classpath:/org/springframework/boot/autoconfigure/info/git-no-data.properties"); - GitInfo gitInfo = this.context.getBean(GitInfo.class); - assertThat(gitInfo.getBranch()).isNull(); + GitProperties gitProperties = this.context.getBean(GitProperties.class); + assertThat(gitProperties.getBranch()).isNull(); } @Test - public void gitInfoFallbackWithGitInfoBean() { - load(CustomGitInfoConfiguration.class, + public void gitPropertiesFallbackWithGitPropertiesBean() { + load(CustomGitPropertiesConfiguration.class, "spring.info.git.location=classpath:/org/springframework/boot/autoconfigure/info/git.properties"); - GitInfo gitInfo = this.context.getBean(GitInfo.class); - assertThat(gitInfo).isSameAs(this.context.getBean("customGitInfo")); + GitProperties gitProperties = this.context.getBean(GitProperties.class); + assertThat(gitProperties).isSameAs(this.context.getBean("customGitProperties")); } private void load(String... environment) { @@ -103,11 +105,11 @@ public class ProjectInfoAutoConfigurationTests { } @Configuration - static class CustomGitInfoConfiguration { + static class CustomGitPropertiesConfiguration { @Bean - public GitInfo customGitInfo() { - return new GitInfo(); + public GitProperties customGitProperties() { + return new GitProperties(new Properties()); } } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 7fd0b7d944..a7d08862da 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -891,6 +891,7 @@ content into your application; rather pick only the properties that you need. management.info.defaults.enabled=true # Enable default health indicators. management.info.env.enabled=true # Enable environment info. management.info.git.enabled=true # Enable git info. + management.info.git.mode=simple # Mode to use to expose git information. # TRACING (({sc-spring-boot-actuator}/trace/TraceProperties.{sc-ext}[TraceProperties]) management.trace.include=request-headers,response-headers,errors # Items to be included in the trace. diff --git a/spring-boot/src/main/java/org/springframework/boot/info/GitProperties.java b/spring-boot/src/main/java/org/springframework/boot/info/GitProperties.java new file mode 100644 index 0000000000..54e180475d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/info/GitProperties.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2016 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.boot.info; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Properties; + +/** + * Provide git-related information such as commit id and time. + * + * @author Stephane Nicoll + * @since 1.4.0 + */ +public class GitProperties extends InfoProperties { + + public GitProperties(Properties entries) { + super(processEntries(entries)); + } + + /** + * Return the name of the branch or {@code null}. + * @return the branch + */ + public String getBranch() { + return get("branch"); + } + + /** + * Return the full id of the commit or {@code null}. + * @return the full commit id + */ + public String getCommitId() { + return get("commit.id"); + } + + /** + * Return the abbreviated id of the commit oir {@code null}. + * @return the short commit id + */ + public String getShortCommitId() { + String commitId = getCommitId(); + return commitId == null ? null + : (commitId.length() > 7 ? commitId.substring(0, 7) : commitId); + } + + /** + * Return the timestamp of the commit, possibly as epoch time in millisecond, or {@code null}. + * @return the commit time + * @see Date#getTime() + */ + public String getCommitTime() { + return get("commit.time"); + } + + private static Properties processEntries(Properties properties) { + coerceDate(properties, "commit.time"); + coerceDate(properties, "build.time"); + return properties; + } + + private static void coerceDate(Properties properties, String key) { + String value = properties.getProperty(key); + if (value != null) { + properties.setProperty(key, coerceToEpoch(value)); + } + } + + /** + * Attempt to convert the specified value to epoch time. Git properties + * information are known to be specified either as epoch time in seconds + * or using a specific date format. + * @param s the value to coerce to + * @return the epoch time in milliseconds or the original value if it couldn't be + * converted + */ + private static String coerceToEpoch(String s) { + Long epoch = parseEpochSecond(s); + if (epoch != null) { + return String.valueOf(epoch); + } + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.US); + try { + return String.valueOf(format.parse(s).getTime()); + } + catch (ParseException ex) { + return s; + } + } + + private static Long parseEpochSecond(String s) { + try { + return Long.parseLong(s) * 1000; + } + catch (NumberFormatException e) { + return null; + } + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/info/InfoProperties.java b/spring-boot/src/main/java/org/springframework/boot/info/InfoProperties.java new file mode 100644 index 0000000000..13d88e454a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/info/InfoProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2016 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.boot.info; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Properties; + +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; + +/** + * Base class for components exposing unstructured data with dedicated methods for + * well known keys. + * + * @author Stephane Nicoll + * @since 1.4.0 + */ +public class InfoProperties implements Iterable { + + private final Properties entries; + + /** + * Create an instance with the specified entries. + * @param entries the information to expose + */ + public InfoProperties(Properties entries) { + Assert.notNull(entries, "Properties must not be null"); + this.entries = copy(entries); + } + + /** + * Return the value of the specified property or {@code null}. + * @param property the id of the property + * @return the property value + */ + public String get(String property) { + return this.entries.getProperty(property); + } + + @Override + public Iterator iterator() { + return new PropertiesIterator(this.entries); + } + + /** + * Return a {@link PropertySource} of this instance. + * @return a {@link PropertySource} + */ + public PropertySource toPropertySource() { + return new PropertiesPropertySource(getClass().getSimpleName(), copy(this.entries)); + } + + private static Properties copy(Properties properties) { + Properties copy = new Properties(); + copy.putAll(properties); + return copy; + } + + /** + * Property entry. + */ + public final class Entry { + + private final String key; + + private final String value; + + private Entry(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return this.key; + } + + public String getValue() { + return this.value; + } + + } + + private final class PropertiesIterator implements Iterator { + + private final Properties properties; + + private final Enumeration keys; + + private PropertiesIterator(Properties properties) { + this.properties = properties; + this.keys = this.properties.keys(); + } + + @Override + public boolean hasNext() { + return this.keys.hasMoreElements(); + } + + @Override + public Entry next() { + String key = (String) this.keys.nextElement(); + return new Entry(key, this.properties.getProperty(key)); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("InfoProperties are immutable."); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/info/GitPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/info/GitPropertiesTests.java new file mode 100644 index 0000000000..0492c717ec --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/info/GitPropertiesTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2016 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.boot.info; + +import java.util.Properties; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GitProperties}. + * + * @author Stephane Nicoll + */ +public class GitPropertiesTests { + + @Test + public void basicInfo() { + GitProperties properties = new GitProperties( + createProperties("master", "abcdefghijklmno", "1457527123")); + assertThat(properties.getBranch()).isEqualTo("master"); + assertThat(properties.getCommitId()).isEqualTo("abcdefghijklmno"); + assertThat(properties.getShortCommitId()).isEqualTo("abcdefg"); + } + + @Test + public void noInfo() { + GitProperties properties = new GitProperties(new Properties()); + assertThat(properties.getBranch()).isNull(); + assertThat(properties.getCommitId()).isNull(); + assertThat(properties.getShortCommitId()).isNull(); + assertThat(properties.getCommitTime()).isNull(); + } + + @Test + public void coerceEpochSecond() { + GitProperties properties = new GitProperties( + createProperties("master", "abcdefg", "1457527123")); + assertThat(properties.getCommitTime()).isEqualTo("1457527123000"); + } + + @Test + public void coerceDateString() { + GitProperties properties = new GitProperties( + createProperties("master", "abcdefg", "2016-03-04T14:36:33+0100")); + assertThat(properties.getCommitTime()).isEqualTo("1457098593000"); + } + + @Test + public void coerceUnsupportedFormat() { + GitProperties properties = new GitProperties( + createProperties("master", "abcdefg", "2016-03-04 15:22:24")); + assertThat(properties.getCommitTime()).isEqualTo("2016-03-04 15:22:24"); + } + + @Test + public void shortenCommitId() { + GitProperties properties = new GitProperties( + createProperties("master", "abc", "1457527123")); + assertThat(properties.getCommitId()).isEqualTo("abc"); + assertThat(properties.getShortCommitId()).isEqualTo("abc"); + } + + private static Properties createProperties(String branch, String commitId, String commitTime) { + Properties properties = new Properties(); + properties.put("branch", branch); + properties.put("commit.id", commitId); + properties.put("commit.time", commitTime); + return properties; + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/info/InfoPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/info/InfoPropertiesTests.java new file mode 100644 index 0000000000..ddb924653a --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/info/InfoPropertiesTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2016 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.boot.info; + +import java.util.Properties; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InfoProperties}. + * + * @author Stephane Nicoll + */ +public class InfoPropertiesTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void inputIsImmutable() { + Properties p = new Properties(); + p.put("foo", "bar"); + InfoProperties infoProperties = new InfoProperties(p); + assertThat(infoProperties.get("foo")).isEqualTo("bar"); + p.remove("foo"); + assertThat(infoProperties.get("foo")).isEqualTo("bar"); + } + + @Test + public void iterator() { + Properties p = new Properties(); + p.put("one", "first"); + p.put("two", "second"); + InfoProperties infoProperties = new InfoProperties(p); + Properties copy = new Properties(); + for (InfoProperties.Entry entry : infoProperties) { + copy.put(entry.getKey(), entry.getValue()); + } + assertThat(p).isEqualTo(copy); + } + + @Test + public void removeNotSupported() { + Properties p = new Properties(); + p.put("foo", "bar"); + InfoProperties infoProperties = new InfoProperties(p); + + this.thrown.expect(UnsupportedOperationException.class); + infoProperties.iterator().remove(); + } + + @Test + public void toPropertySources() { + Properties p = new Properties(); + p.put("one", "first"); + p.put("two", "second"); + InfoProperties infoProperties = new MyInfoProperties(p); + PropertySource source = infoProperties.toPropertySource(); + assertThat(source.getProperty("one")).isEqualTo("first"); + assertThat(source.getProperty("two")).isEqualTo("second"); + assertThat(source.getName()).isEqualTo("MyInfoProperties"); + } + + private static class MyInfoProperties extends InfoProperties { + + MyInfoProperties(Properties entries) { + super(entries); + } + } + +}