@ -16,21 +16,32 @@
package org.springframework.boot.actuate.endpoint ;
package org.springframework.boot.actuate.endpoint ;
import java.io.IOException ;
import java.util.ArrayList ;
import java.util.ArrayList ;
import java.util.Collection ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.HashMap ;
import java.util.HashSet ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.Set ;
import org.apache.commons.logging.Log ;
import org.apache.commons.logging.LogFactory ;
import org.springframework.beans.BeanWrapperImpl ;
import org.springframework.beans.BeansException ;
import org.springframework.beans.BeansException ;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData ;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData ;
import org.springframework.boot.context.properties.ConfigurationProperties ;
import org.springframework.boot.context.properties.ConfigurationProperties ;
import org.springframework.context.ApplicationContext ;
import org.springframework.context.ApplicationContext ;
import org.springframework.context.ApplicationContextAware ;
import org.springframework.context.ApplicationContextAware ;
import org.springframework.core.convert.ConversionService ;
import org.springframework.core.io.Resource ;
import org.springframework.core.convert.support.DefaultConversionService ;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver ;
import org.springframework.util.ClassUtils ;
import org.springframework.util.StringUtils ;
import org.springframework.util.StringUtils ;
import com.fasterxml.jackson.core.JsonParseException ;
import com.fasterxml.jackson.databind.BeanDescription ;
import com.fasterxml.jackson.databind.BeanDescription ;
import com.fasterxml.jackson.databind.JsonMappingException ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.SerializationConfig ;
import com.fasterxml.jackson.databind.SerializationConfig ;
import com.fasterxml.jackson.databind.SerializationFeature ;
import com.fasterxml.jackson.databind.SerializationFeature ;
@ -64,12 +75,17 @@ public class ConfigurationPropertiesReportEndpoint extends
private static final String CGLIB_FILTER_ID = "cglibFilter" ;
private static final String CGLIB_FILTER_ID = "cglibFilter" ;
private static final Log logger = LogFactory
. getLog ( ConfigurationPropertiesReportEndpoint . class ) ;
private final Sanitizer sanitizer = new Sanitizer ( ) ;
private final Sanitizer sanitizer = new Sanitizer ( ) ;
private ApplicationContext context ;
private ApplicationContext context ;
private ConfigurationBeanFactoryMetaData beanFactoryMetaData ;
private ConfigurationBeanFactoryMetaData beanFactoryMetaData ;
private ConfigurationPropertiesMetaData metadata = new ConfigurationPropertiesMetaData ( ) ;
public ConfigurationPropertiesReportEndpoint ( ) {
public ConfigurationPropertiesReportEndpoint ( ) {
super ( "configprops" ) ;
super ( "configprops" ) ;
}
}
@ -97,7 +113,6 @@ public class ConfigurationPropertiesReportEndpoint extends
* Extract beans annotated { @link ConfigurationProperties } and serialize into
* Extract beans annotated { @link ConfigurationProperties } and serialize into
* { @link Map } .
* { @link Map } .
* /
* /
@SuppressWarnings ( "unchecked" )
protected Map < String , Object > extract ( ApplicationContext context ) {
protected Map < String , Object > extract ( ApplicationContext context ) {
Map < String , Object > result = new HashMap < String , Object > ( ) ;
Map < String , Object > result = new HashMap < String , Object > ( ) ;
@ -116,8 +131,9 @@ public class ConfigurationPropertiesReportEndpoint extends
String beanName = entry . getKey ( ) ;
String beanName = entry . getKey ( ) ;
Object bean = entry . getValue ( ) ;
Object bean = entry . getValue ( ) ;
Map < String , Object > root = new HashMap < String , Object > ( ) ;
Map < String , Object > root = new HashMap < String , Object > ( ) ;
root . put ( "prefix" , extractPrefix ( context , beanName , bean ) ) ;
String prefix = extractPrefix ( context , beanName , bean ) ;
root . put ( "properties" , sanitize ( mapper . convertValue ( bean , Map . class ) ) ) ;
root . put ( "prefix" , prefix ) ;
root . put ( "properties" , sanitize ( safeSerialize ( mapper , bean , prefix ) ) ) ;
result . put ( beanName , root ) ;
result . put ( beanName , root ) ;
}
}
@ -128,12 +144,31 @@ public class ConfigurationPropertiesReportEndpoint extends
return result ;
return result ;
}
}
/ * *
* Cautiously serialize the bean to a map ( returning a map with an error message
* instead of throwing an exception if there is a problem ) .
* /
private Map < String , Object > safeSerialize ( ObjectMapper mapper , Object bean ,
String prefix ) {
try {
@SuppressWarnings ( "unchecked" )
Map < String , Object > result = new HashMap < String , Object > ( mapper . convertValue (
this . metadata . extractMap ( bean , prefix ) , Map . class ) ) ;
return result ;
}
catch ( Exception e ) {
return new HashMap < String , Object > ( Collections . < String , Object > singletonMap (
"error" , "Cannot serialize '" + prefix + "'" ) ) ;
}
}
/ * *
/ * *
* Configure Jackson ' s { @link ObjectMapper } to be used to serialize the
* Configure Jackson ' s { @link ObjectMapper } to be used to serialize the
* { @link ConfigurationProperties } objects into a { @link Map } structure .
* { @link ConfigurationProperties } objects into a { @link Map } structure .
* /
* /
protected void configureObjectMapper ( ObjectMapper mapper ) {
protected void configureObjectMapper ( ObjectMapper mapper ) {
mapper . configure ( SerializationFeature . FAIL_ON_EMPTY_BEANS , false ) ;
mapper . configure ( SerializationFeature . FAIL_ON_EMPTY_BEANS , false ) ;
mapper . configure ( SerializationFeature . WRITE_NULL_MAP_VALUES , false ) ;
applyCglibFilters ( mapper ) ;
applyCglibFilters ( mapper ) ;
applySerializationModifier ( mapper ) ;
applySerializationModifier ( mapper ) ;
}
}
@ -148,7 +183,7 @@ public class ConfigurationPropertiesReportEndpoint extends
}
}
/ * *
/ * *
* Configure PropertyFil er to make sure Jackson doesn ' t process CGLIB generated bean
* Configure PropertyFil t er to make sure Jackson doesn ' t process CGLIB generated bean
* properties .
* properties .
* /
* /
private void applyCglibFilters ( ObjectMapper mapper ) {
private void applyCglibFilters ( ObjectMapper mapper ) {
@ -239,25 +274,182 @@ public class ConfigurationPropertiesReportEndpoint extends
protected static class GenericSerializerModifier extends BeanSerializerModifier {
protected static class GenericSerializerModifier extends BeanSerializerModifier {
private ConversionService conversionService = new DefaultConversionService ( ) ;
@Override
@Override
public List < BeanPropertyWriter > changeProperties ( SerializationConfig config ,
public List < BeanPropertyWriter > changeProperties ( SerializationConfig config ,
BeanDescription beanDesc , List < BeanPropertyWriter > beanProperties ) {
BeanDescription beanDesc , List < BeanPropertyWriter > beanProperties ) {
List < BeanPropertyWriter > result = new ArrayList < BeanPropertyWriter > ( ) ;
List < BeanPropertyWriter > result = new ArrayList < BeanPropertyWriter > ( ) ;
for ( BeanPropertyWriter writer : beanProperties ) {
for ( BeanPropertyWriter writer : beanProperties ) {
AnnotatedMethod setter = beanDesc . findMethod (
boolean readable = isReadable ( beanDesc , writer ) ;
"set" + StringUtils . capitalize ( writer . getName ( ) ) ,
if ( readable ) {
new Class < ? > [ ] { writer . getPropertyType ( ) } ) ;
if ( setter ! = null
& & this . conversionService . canConvert ( String . class ,
writer . getPropertyType ( ) ) ) {
result . add ( writer ) ;
result . add ( writer ) ;
}
}
}
}
return result ;
return result ;
}
}
private boolean isReadable ( BeanDescription beanDesc , BeanPropertyWriter writer ) {
String parenType = beanDesc . getType ( ) . getTypeName ( ) ;
String type = writer . getType ( ) . getTypeName ( ) ;
AnnotatedMethod setter = beanDesc . findMethod (
"set" + StringUtils . capitalize ( writer . getName ( ) ) ,
new Class < ? > [ ] { writer . getPropertyType ( ) } ) ;
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that its a nested class used solely for binding to config props, so it
// should be kosher. This filter is not used if there is JSON metadata for
// the property, so it's mainly for user-defined beans.
boolean readable = setter ! = null
| | ClassUtils . getPackageName ( parenType ) . equals (
ClassUtils . getPackageName ( type ) ) ;
return readable ;
}
}
/ * *
* Convenience class for grabbing and caching valid property names from
* / META - INF / spring - configuration - metadata . json so that metadata that is known to be
* valid can be used to pull the correct nested properties out of beans that might
* otherwise be tricky ( contain cycles or other unserializable properties ) .
* /
protected static class ConfigurationPropertiesMetaData {
private Map < String , Set < String > > matched = new HashMap < String , Set < String > > ( ) ;
private Set < String > keys = null ;
public boolean matches ( String prefix ) {
if ( this . matched . containsKey ( prefix ) ) {
return matchesInternal ( prefix ) ;
}
synchronized ( this . matched ) {
if ( this . matched . containsKey ( prefix ) ) {
return matchesInternal ( prefix ) ;
}
this . matched . put ( prefix , findKeys ( prefix ) ) ;
}
return matchesInternal ( prefix ) ;
}
private boolean matchesInternal ( String prefix ) {
if ( this . matched . get ( prefix ) ! = null ) {
return true ;
}
else {
return false ;
}
}
private Set < String > findKeys ( String prefix ) {
HashSet < String > set = new HashSet < String > ( ) ;
try {
if ( this . keys = = null ) {
this . keys = new HashSet < String > ( ) ;
ObjectMapper mapper = new ObjectMapper ( ) ;
Resource [ ] resources = new PathMatchingResourcePatternResolver ( )
. getResources ( "classpath*:/META-INF/*spring-configuration-metadata.json" ) ;
for ( Resource resource : resources ) {
addKeys ( mapper , resource ) ;
}
}
}
catch ( IOException e ) {
logger . warn ( "Could not deserialize config properties metadata" , e ) ;
}
for ( String key : this . keys ) {
if ( key . length ( ) > prefix . length ( )
& & key . startsWith ( prefix )
& & "." . equals ( key . substring ( prefix . length ( ) , prefix . length ( ) + 1 ) ) ) {
set . add ( key . substring ( prefix . length ( ) + 1 ) ) ;
}
}
if ( set . isEmpty ( ) ) {
return null ;
}
return set ;
}
private void addKeys ( ObjectMapper mapper , Resource resource ) throws IOException ,
JsonParseException , JsonMappingException {
@SuppressWarnings ( "unchecked" )
Map < String , Object > map = mapper . readValue ( resource . getInputStream ( ) ,
Map . class ) ;
@SuppressWarnings ( "unchecked" )
Collection < Map < String , Object > > metadata = ( Collection < Map < String , Object > > ) map
. get ( "properties" ) ;
for ( Map < String , Object > value : metadata ) {
try {
if ( value . containsKey ( "type" ) ) {
this . keys . add ( ( String ) value . get ( "name" ) ) ;
}
}
catch ( Exception e ) {
logger . warn ( "Could not parse config properties metadata" , e ) ;
}
}
}
public Object extractMap ( Object bean , String prefix ) {
if ( ! matches ( prefix ) ) {
return bean ;
}
Map < String , Object > map = new HashMap < String , Object > ( ) ;
for ( String key : this . matched . get ( prefix ) ) {
addProperty ( bean , key , map ) ;
}
return map ;
}
private void addProperty ( Object bean , String key , Map < String , Object > map ) {
String prefix = key . contains ( "." ) ? StringUtils . split ( key , "." ) [ 0 ] : key ;
String suffix = key . length ( ) > prefix . length ( ) ? key . substring ( prefix
. length ( ) + 1 ) : null ;
String property = prefix ;
if ( bean instanceof Map ) {
@SuppressWarnings ( "unchecked" )
Map < String , Object > value = ( Map < String , Object > ) bean ;
bean = new MapHolder ( value ) ;
property = "map[" + property + "]" ;
}
BeanWrapperImpl wrapper = new BeanWrapperImpl ( bean ) ;
try {
Object value = wrapper . getPropertyValue ( property ) ;
if ( value instanceof Map ) {
Map < String , Object > nested = new HashMap < String , Object > ( ) ;
map . put ( prefix , nested ) ;
if ( suffix ! = null ) {
addProperty ( value , suffix , nested ) ;
}
}
else {
map . put ( prefix , value ) ;
}
}
catch ( Exception e ) {
// Probably just lives on a different bean (it happens)
logger . debug ( "Could not parse config properties metadata '" + key + "': "
+ e . getMessage ( ) ) ;
}
}
protected static class MapHolder {
Map < String , Object > map = new HashMap < String , Object > ( ) ;
public MapHolder ( Map < String , Object > bean ) {
this . map . putAll ( bean ) ;
}
public Map < String , Object > getMap ( ) {
return this . map ;
}
public void setMap ( Map < String , Object > map ) {
this . map = map ;
}
}
}
}
}
}