├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── morgan │ │ └── design │ │ ├── properties │ │ ├── ReloadableProperty.java │ │ ├── bean │ │ │ ├── BeanPropertyHolder.java │ │ │ └── PropertyModifiedEvent.java │ │ ├── conversion │ │ │ ├── CustomEditorsRegistrar.java │ │ │ ├── DefaultPropertyConversionService.java │ │ │ └── PropertyConversionService.java │ │ ├── event │ │ │ ├── GuavaPropertyChangedEventNotifier.java │ │ │ └── PropertyChangedEventNotifier.java │ │ ├── internal │ │ │ ├── PropertiesWatcher.java │ │ │ ├── ReadablePropertySourcesPlaceholderConfigurer.java │ │ │ └── ReloadablePropertyPostProcessor.java │ │ └── resolver │ │ │ ├── PropertyResolver.java │ │ │ └── SubstitutingPropertyResolver.java │ │ └── util │ │ └── JodaUtils.java └── resources │ ├── app.properties │ ├── logback.xml │ └── spring │ ├── spring-defaultConfiguration.xml │ └── spring-reloadableProperties.xml └── test ├── java └── com │ └── morgan │ └── design │ ├── properties │ ├── conversion │ │ └── DefaultPropertyConversionServiceUnitTest.java │ ├── event │ │ └── GuavaPropertyChangedEventNotifierUnitTest.java │ ├── internal │ │ ├── FailingReloadablePropertyPostProcessorIntTest.java │ │ ├── PropertiesWatcherUnitTest.java │ │ ├── ReloadablePropertyPostProcessorIntTest.java │ │ └── UpdatingReloadablePropertyPostProcessorIntTest.java │ ├── resolver │ │ └── SubstitutingPropertyResolverUnitTest.java │ └── testBeans │ │ ├── AutowiredPropertyBean.java │ │ ├── BadValue.java │ │ ├── FinalFieldBean.java │ │ ├── MissingProperty.java │ │ └── ReloadingAutowiredPropertyBean.java │ └── util │ └── JodaUtilsUnitTest.java └── resources ├── logback-test.xml ├── spring ├── spring-badValue.xml ├── spring-finalFieldBean.xml ├── spring-missingProperty.xml ├── spring-reloadablePropertyPostProcessorIntTest.xml └── spring-reloading-reloadablePropertyPostProcessorIntTest.xml ├── test-files ├── different_fileWatcher.properties ├── example.properties ├── fileWatcher.properties └── reloading.properties └── test-files2 └── fileWatcher2.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # Eclipse # 9 | .settings 10 | .project 11 | .checkstyle 12 | .springbeans 13 | .classpath 14 | target/* 15 | /target 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2014] [James Morgan (Morgan-Design)] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Reloadable Properties Annotation ## 2 | 3 | A simple utilty which allows object fields to be set from properties files via a @ReloadableProperty annotation. 4 | These properties also auto reload if the given properties file changes during runtime. 5 | 6 | ### Example Annotation Usage ### 7 |
  8 | 	@ReloadableProperty("dynamicProperty.longValue")
  9 | 	private long primitiveWithDefaultValue = 55;
 10 | 	
 11 | 	@ReloadableProperty("dynamicProperty.substitutionValue")
 12 | 	private String stringProperty;
 13 | 	
 14 | 	@ReloadableProperty("dynamicProperty.compoiteStringValue")
 15 | 	private String compsiteStringProperty;
 16 | 
17 | 18 | ### Example Properties File ### 19 |
 20 | 	dynamicProperty.longValue=12345
 21 | 	dynamicProperty.substitutionProperty=${dynamicProperty.substitutionValue}
 22 | 	dynamicProperty.compoiteStringValue=Hello, ${dynamicProperty.baseStringValue}!
 23 | 
24 | 25 | ### Example Spring XML Configuration ### 26 | * See [spring-reloadableProperties.xml](https://github.com/jamesemorgan/ReloadablePropertiesAnnotation/blob/master/src/main/resources/spring/spring-reloadableProperties.xml) for example configuration 27 | * All main components can be extended or replaced if required 28 | 29 | ### How it Works ### 30 | When Spring starts an Application Context an implementation of Springs [PropertySourcesPlaceholderConfigurer](http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.html) is instantiated to perform additional logic when loading and setting values from a given set of properties files. (see: [ReadablePropertySourcesPlaceholderConfigurer](https://github.com/jamesemorgan/ReloadablePropertiesAnnotation/blob/master/src/main/java/com/morgan/design/properties/internal/ReadablePropertySourcesPlaceholderConfigurer.java)) 31 | 32 | During the instantiation phasae of an Application Context a new instance of [InstantiationAwareBeanPostProcessorAdapter](http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.html) is also created which allows post bean processing to occur. 33 | 34 | Google Guava is used to implement a simple Publish & Subscribe (Pub-Sub) Pattern so that beans can be updated once created, i.e. a bean can subscribe to property change events. (see: [EventBus](http://code.google.com/p/guava-libraries/wiki/EventBusExplained)) 35 | EventBus was chosen as it is a very easy and simplistic way to implement loosely couple object structure. (see: [blog](http://codingjunkie.net/guava-eventbus/)) 36 | 37 | When each properties file resource is loaded a [PropertiesWatcher](https://github.com/jamesemorgan/ReloadablePropertiesAnnotation/blob/master/src/main/java/com/morgan/design/properties/internal/PropertiesWatcher.java) is started and attached to the given resource set, reporting on any [java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY](http://docs.oracle.com/javase/7/docs/api/java/nio/file/StandardWatchEventKinds.html#ENTRY_MODIFY) events from the host operating system 38 | 39 | When an ENTRY_MODIFY event is fired firstly the resource changed is checked for property value changes then any bean subscribing to changes to the modified property has the specified field value updated with the new property. Once the filed value is updated no other operations are performed on the object. 40 | 41 | Each resource specified starts a new thread per parent directory i.e. two properties files in the same directory requires only one ResourceWatcher thread, three properties files in three different directories will start three threads. 42 | 43 | ### Tests ### 44 | A set of integration and unit tests can be found in _src/test/java_ (tests) & _src/test/resources_ (test resources) 45 | 46 | ### TODO (Unfinished) ### 47 | * Update test method names 48 | * Creation of any test utilities or helper classes 49 | 50 | ### Why? ### 51 | * Useful for web applications which often need configuration changes but you don't always want to restart the application before new properties are used. 52 | * Can be used to define several layers of properties which can aid in defining multiple application configurations e.g sandbox/development/testing/production. 53 | * A pet project of mine I have been intending to implement for a while 54 | * A test of the new Java 7 WatchService API 55 | * Another dive in Spring & general investigation of Google Guava's EventBus 56 | * The project is aimed to be open to modification if required 57 | * Sample testing tools (CountDownLatch, Hamcrest-1.3, JMock-2.6.0-RC2) 58 | 59 | ### Future Changes ### 60 | * Ability to use Spring Expression language to map properties files 61 | * Support for Java 7 Date and Time classes 62 | * Include the ability to define a database driven properties source not just properties files 63 | * Implement error recovery inside PropertiesWatcher.class, including better thread recovery 64 | * Ability to perform additional re-bind logic when a property is changed, i.e. if a class has an open DB connection which needs to be re-established using newly set properties. 65 | * Replace callback Properties EventHandler with Guava EventBus 66 | * Ability to configure usage via spring's @Configuration 67 | 68 | ### Contributions ### 69 | * Thank you [normanatashbar](https://github.com/normanatashbar) for adding composite string replacement 70 | * Thank you [shiva2991](https://github.com/normanatashbar) for adding java.util.Date type conversion. 71 | 72 | ### Supported Property Type Conversions Available ### 73 | * LocalDate.class 74 | * LocalTime.class 75 | * LocalDateTime.class 76 | * Period.class 77 | 78 | 79 | * Spring Supported (3.1.2-RELEASE) 80 | * String.class 81 | * Date.class 82 | * boolean.class, Boolean.class 83 | * byte.class, Byte.class 84 | * char.class, Character.class 85 | * short.class, Short.class 86 | * int.class, Integer.class 87 | * long.class,Long.class 88 | * float.class, Float.class 89 | * double.class, Double.class 90 | 91 | ### Dependencies ### 92 | 93 | #### Core #### 94 | * Java 7 SDK 95 | * Spring (3.2.5-RELEASE) 96 | * Google Guava (14.0.1) 97 | * Joda Time Library (2.1) - [link](http://joda-time.sourceforge.net/) 98 | 99 | #### Logging #### 100 | * logback (1.0.13) 101 | * slf4j (1.7.5) 102 | 103 | #### Testing #### 104 | * juint (4.11) 105 | * jmock (2.6.0) 106 | * hamcrest-all (1.3) 107 | * spring-test (3.2.5-RELEASE) 108 | 109 | 110 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/jamesmorgan/reloadablepropertiesannotation/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 111 | 112 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.morgan.design 5 | ReloadablePropertiesAnnotation 6 | 0.0.2-SNAPSHOT 7 | 8 | 9 | 3.2.5.RELEASE 10 | 1.0.13 11 | 12 | 13 | 14 | 15 | 16 | com.google.guava 17 | guava 18 | 14.0.1 19 | 20 | 21 | joda-time 22 | joda-time 23 | 2.3 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | 1.7.5 29 | 30 | 31 | ch.qos.logback 32 | logback-core 33 | ${logback.version} 34 | 35 | 36 | ch.qos.logback 37 | logback-classic 38 | ${logback.version} 39 | 40 | 41 | 42 | 43 | org.springframework 44 | spring-context 45 | ${spring.version} 46 | 47 | 48 | org.springframework 49 | spring-core 50 | ${spring.version} 51 | 52 | 53 | 54 | 55 | junit 56 | junit 57 | test 58 | 4.11 59 | 60 | 61 | org.hamcrest 62 | hamcrest-all 63 | test 64 | 1.3 65 | 66 | 67 | org.jmock 68 | jmock 69 | 2.6.0 70 | test 71 | 72 | 73 | org.jmock 74 | jmock-legacy 75 | 2.6.0 76 | test 77 | 78 | 79 | org.jmock 80 | jmock-junit4 81 | 2.6.0 82 | test 83 | 84 | 85 | org.springframework 86 | spring-test 87 | ${spring.version} 88 | test 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-eclipse-plugin 98 | 2.6 99 | 100 | true 101 | true 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-clean-plugin 107 | 2.4.1 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-compiler-plugin 112 | 2.3.2 113 | 114 | 115 | compile 116 | compile 117 | 118 | compile 119 | 120 | 121 | 122 | unit-test-compile 123 | 124 | testCompile 125 | 126 | 127 | 128 | 129 | 1.7 130 | 1.7 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-jar-plugin 136 | 2.4 137 | 138 | 139 | **/logback.xml 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | central 149 | Maven Plugin Repository 150 | http://repo1.maven.org/maven2 151 | 152 | true 153 | 154 | 155 | true 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/ReloadableProperty.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a field to be set from the given property value, the specified property will reset the field if changed during runtime. 10 | * 11 | * @author James Morgan 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.FIELD) 15 | public @interface ReloadableProperty { 16 | 17 | String value(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/bean/BeanPropertyHolder.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.bean; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | import com.google.common.base.Objects; 6 | 7 | public class BeanPropertyHolder { 8 | 9 | private final Object bean; 10 | private final Field field; 11 | 12 | public BeanPropertyHolder(Object bean, Field field) { 13 | this.bean = bean; 14 | this.field = field; 15 | } 16 | 17 | public Object getBean() { 18 | return this.bean; 19 | } 20 | 21 | public Field getField() { 22 | return this.field; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return Objects.hashCode(this.bean, this.field); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object object) { 32 | if (object instanceof BeanPropertyHolder) { 33 | BeanPropertyHolder that = (BeanPropertyHolder) object; 34 | return Objects.equal(this.bean, that.bean) && Objects.equal(this.field, that.field); 35 | } 36 | return false; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return Objects.toStringHelper(this) 42 | .add("bean", this.bean) 43 | .add("field", this.field) 44 | .toString(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/bean/PropertyModifiedEvent.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.bean; 2 | 3 | import com.google.common.base.Objects; 4 | 5 | public class PropertyModifiedEvent { 6 | 7 | private final String propertyName; 8 | private final Object oldValue; 9 | private final Object newValue; 10 | 11 | public PropertyModifiedEvent(final String propertyName, final Object oldValue, final Object newValue) { 12 | this.propertyName = propertyName; 13 | this.oldValue = oldValue; 14 | this.newValue = newValue; 15 | } 16 | 17 | public String getPropertyName() { 18 | return this.propertyName; 19 | } 20 | 21 | public Object getOldValue() { 22 | return this.oldValue; 23 | } 24 | 25 | public Object getNewValue() { 26 | return this.newValue; 27 | } 28 | @Override 29 | public int hashCode() { 30 | return Objects.hashCode(this.propertyName, this.oldValue, this.newValue); 31 | } 32 | 33 | @Override 34 | public boolean equals(final Object object) { 35 | if (object instanceof PropertyModifiedEvent) { 36 | final PropertyModifiedEvent that = (PropertyModifiedEvent) object; 37 | return Objects.equal(this.propertyName, that.propertyName) && Objects.equal(this.oldValue, that.oldValue) 38 | && Objects.equal(this.newValue, that.newValue); 39 | } 40 | return false; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return Objects.toStringHelper(this) 46 | .add("propertyName", this.propertyName) 47 | .add("oldValue", this.oldValue) 48 | .add("newValue", this.newValue) 49 | .toString(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/conversion/CustomEditorsRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.conversion; 2 | 3 | import java.beans.PropertyEditor; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.PropertyEditorRegistrar; 7 | import org.springframework.beans.PropertyEditorRegistry; 8 | 9 | public class CustomEditorsRegistrar implements PropertyEditorRegistrar { 10 | 11 | private Map, PropertyEditor> extraEditors; 12 | 13 | public CustomEditorsRegistrar(Map, PropertyEditor> extraEditors) 14 | { 15 | this.extraEditors = extraEditors; 16 | } 17 | 18 | @Override 19 | public void registerCustomEditors(PropertyEditorRegistry registry) { 20 | for (Map.Entry, PropertyEditor> entry : extraEditors.entrySet()) { 21 | registry.registerCustomEditor(entry.getKey(), entry.getValue()); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/conversion/DefaultPropertyConversionService.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.conversion; 2 | 3 | import java.lang.reflect.Field; 4 | import java.util.Map; 5 | 6 | import javax.annotation.PostConstruct; 7 | 8 | import org.joda.time.LocalDate; 9 | import org.joda.time.LocalDateTime; 10 | import org.joda.time.LocalTime; 11 | import org.joda.time.Period; 12 | import org.springframework.beans.SimpleTypeConverter; 13 | import org.springframework.beans.TypeConverter; 14 | import org.springframework.beans.factory.BeanInitializationException; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 17 | import org.springframework.stereotype.Component; 18 | 19 | import com.google.common.base.Function; 20 | import com.google.common.base.Functions; 21 | import com.google.common.collect.Maps; 22 | import com.morgan.design.util.JodaUtils; 23 | 24 | /** 25 | * Default implementation of {@link PropertyConversionService}, attempting to convert an object otherwise utilising {@link SimpleTypeConverter} if no matching 26 | * converter is found. 27 | * 28 | * @author James Morgan 29 | */ 30 | @Component 31 | public class DefaultPropertyConversionService implements PropertyConversionService { 32 | 33 | @Autowired 34 | private ConfigurableBeanFactory configurableBeanFactory; 35 | private static TypeConverter DEFAULT; 36 | private static Map, Function> CONVERTS = Maps.newHashMap(); 37 | static { 38 | CONVERTS.put(Period.class, new PeriodConverter()); 39 | CONVERTS.put(LocalDateTime.class, new LocalDateTimeConverter()); 40 | CONVERTS.put(LocalDate.class, new LocalDateConverter()); 41 | CONVERTS.put(LocalTime.class, new LocalTimeConverter()); 42 | } 43 | 44 | @PostConstruct 45 | public void init() { 46 | DEFAULT = configurableBeanFactory.getTypeConverter(); 47 | } 48 | 49 | @Override 50 | public Object convertPropertyForField(final Field field, final Object property) { 51 | try { 52 | return Functions.forMap(CONVERTS, new DefaultConverter(field.getType())) 53 | .apply(field.getType()) 54 | .apply(property); 55 | } 56 | catch (final Throwable e) { 57 | throw new BeanInitializationException(String.format("Unable to convert property for field [%s]. Value [%s] cannot be converted to [%s]", 58 | field.getName(), property, field.getType()), e); 59 | } 60 | } 61 | 62 | private static class DefaultConverter implements Function { 63 | private final Class type; 64 | 65 | public DefaultConverter(final Class type) { 66 | this.type = type; 67 | } 68 | 69 | @Override 70 | public Object apply(final Object input) { 71 | return DEFAULT.convertIfNecessary(input, this.type); 72 | } 73 | } 74 | 75 | private static class PeriodConverter implements Function { 76 | @Override 77 | public Period apply(final Object input) { 78 | return JodaUtils.timeStringToPeriodOrNull((String) input); 79 | } 80 | } 81 | 82 | private static class LocalDateTimeConverter implements Function { 83 | @Override 84 | public LocalDateTime apply(final Object input) { 85 | return JodaUtils.timestampStringToLocalDateTimeOrNull((String) input); 86 | } 87 | } 88 | 89 | private static class LocalDateConverter implements Function { 90 | @Override 91 | public LocalDate apply(final Object input) { 92 | return JodaUtils.dateStringToLocalDateOrNull((String) input); 93 | } 94 | } 95 | 96 | private static class LocalTimeConverter implements Function { 97 | @Override 98 | public LocalTime apply(final Object input) { 99 | return JodaUtils.timeStringToLocalTimeOrNull((String) input); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/conversion/PropertyConversionService.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.conversion; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | /** 6 | * Interface intended for use by any class willing to convert the given property {@link Object} which potentially requires conversion before being set on the 7 | * given {@link Field} 8 | * 9 | * @author James Morgan 10 | */ 11 | public interface PropertyConversionService { 12 | 13 | /** 14 | * @param field the destination filed to set the property on 15 | * @param property the property to be converted for the given field 16 | * @return the potentially converted field 17 | */ 18 | Object convertPropertyForField(final Field field, final Object property); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/event/GuavaPropertyChangedEventNotifier.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.event; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Component; 6 | 7 | import com.google.common.eventbus.EventBus; 8 | import com.morgan.design.properties.bean.PropertyModifiedEvent; 9 | import com.morgan.design.properties.internal.ReloadablePropertyPostProcessor; 10 | 11 | @Component 12 | public class GuavaPropertyChangedEventNotifier implements PropertyChangedEventNotifier { 13 | 14 | private final EventBus eventBus; 15 | 16 | @Autowired 17 | public GuavaPropertyChangedEventNotifier(@Qualifier("propertiesEventBus") final EventBus eventBus) { 18 | this.eventBus = eventBus; 19 | } 20 | 21 | @Override 22 | public void post(final PropertyModifiedEvent propertyChangedEvent) { 23 | this.eventBus.post(propertyChangedEvent); 24 | } 25 | 26 | @Override 27 | public void unregister(final ReloadablePropertyPostProcessor ReloadablePropertyPostProcessor) { 28 | this.eventBus.unregister(ReloadablePropertyPostProcessor); 29 | } 30 | 31 | @Override 32 | public void register(final ReloadablePropertyPostProcessor ReloadablePropertyPostProcessor) { 33 | this.eventBus.register(ReloadablePropertyPostProcessor); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/event/PropertyChangedEventNotifier.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.event; 2 | 3 | import com.morgan.design.properties.bean.PropertyModifiedEvent; 4 | import com.morgan.design.properties.internal.ReloadablePropertyPostProcessor; 5 | 6 | public interface PropertyChangedEventNotifier { 7 | 8 | void post(PropertyModifiedEvent propertyChangedEvent); 9 | 10 | void unregister(ReloadablePropertyPostProcessor reloadablePropertyProcessor); 11 | 12 | void register(ReloadablePropertyPostProcessor reloadablePropertyProcessor); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/internal/PropertiesWatcher.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.ClosedWatchServiceException; 7 | import java.nio.file.FileSystems; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.nio.file.WatchEvent; 11 | import java.nio.file.WatchEvent.Kind; 12 | import java.nio.file.WatchKey; 13 | import java.nio.file.WatchService; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.core.io.Resource; 23 | 24 | import com.google.common.collect.Maps; 25 | 26 | public class PropertiesWatcher implements Runnable { 27 | 28 | protected static Logger log = LoggerFactory.getLogger(PropertiesWatcher.class); 29 | 30 | public interface EventPublisher { 31 | void onResourceChanged(Resource resource); 32 | } 33 | 34 | private final Resource[] locations; 35 | private final EventPublisher eventPublisher; 36 | 37 | private WatchService watchService; 38 | final ExecutorService service; 39 | 40 | public PropertiesWatcher(final Resource[] locations, final EventPublisher eventPublisher) throws IOException { 41 | this.locations = locations; 42 | this.eventPublisher = eventPublisher; 43 | this.watchService = FileSystems.getDefault() 44 | .newWatchService(); 45 | this.service = Executors.newCachedThreadPool(); 46 | } 47 | 48 | @Override 49 | public void run() { 50 | final Map> pathsAndResources = findAvailableResourcePaths(); 51 | for (final Path pathToWatch : pathsAndResources.keySet()) { 52 | final List availableResources = pathsAndResources.get(pathToWatch); 53 | log.debug("Starting ResourceWatcher on file {}", availableResources); 54 | this.service.submit(new ResourceWatcher(pathToWatch, availableResources)); 55 | } 56 | } 57 | 58 | public void stop() { 59 | try { 60 | log.debug("Closing File Watching Service"); 61 | this.watchService.close(); 62 | 63 | log.debug("Shuting down Thread Service"); 64 | this.service.shutdownNow(); 65 | } 66 | catch (final IOException e) { 67 | log.error("Unable to stop file watcher", e); 68 | } 69 | } 70 | 71 | private Map> findAvailableResourcePaths() { 72 | final Map> map = Maps.newHashMap(); 73 | for (final Resource resource : this.locations) { 74 | final Path resourceParentPath = getResourceParentPath(resource); 75 | if (null == map.get(resourceParentPath)) { 76 | map.put(resourceParentPath, new ArrayList()); 77 | } 78 | map.get(resourceParentPath) 79 | .add(resource); 80 | } 81 | return map; 82 | } 83 | 84 | private Path getResourceParentPath(final Resource resource) { 85 | try { 86 | return Paths.get(resource.getFile() 87 | .getParentFile() 88 | .toURI()); 89 | } 90 | catch (final IOException e) { 91 | log.error("Unable to get resource path", e); 92 | } 93 | return null; 94 | } 95 | 96 | private void publishResourceChangedEvent(final Resource resource) { 97 | this.eventPublisher.onResourceChanged(resource); 98 | } 99 | 100 | private WatchService getWatchService() { 101 | return this.watchService; 102 | } 103 | 104 | private class ResourceWatcher implements Runnable { 105 | 106 | private final Path path; 107 | private final List resources; 108 | 109 | public ResourceWatcher(final Path path, final List resources) { 110 | this.path = path; 111 | this.resources = resources; 112 | } 113 | 114 | @Override 115 | public void run() { 116 | try { 117 | log.debug("START"); 118 | log.debug("Watching for modifcation events for path {}", this.path.toString()); 119 | while (!Thread.currentThread() 120 | .isInterrupted()) { 121 | final WatchKey pathBeingWatched = this.path.register(getWatchService(), ENTRY_MODIFY); 122 | 123 | WatchKey watchKey = null; 124 | try { 125 | watchKey = getWatchService().take(); 126 | } 127 | catch (final ClosedWatchServiceException | InterruptedException e) { 128 | log.debug("END"); 129 | Thread.currentThread() 130 | .interrupt(); 131 | } 132 | 133 | if (watchKey != null) { 134 | for (final WatchEvent event : pathBeingWatched.pollEvents()) { 135 | log.debug("File modification Event Triggered"); 136 | final Path target = path(event.context()); 137 | if (isValidTargetFile(target)) { 138 | final Path watchedPath = path(watchKey.watchable()); 139 | final Kind eventKind = event.kind(); 140 | 141 | logNewEvent(watchedPath, eventKind, target); 142 | publishResourceChangedEvent(getResource(target)); 143 | } 144 | } 145 | if (!watchKey.reset()) { 146 | log.debug("END"); 147 | Thread.currentThread() 148 | .interrupt(); 149 | return; 150 | } 151 | } 152 | } 153 | } 154 | catch (final Exception e) { 155 | log.error("Exception thrown when watching resources, path {}\nException:", this.path.toString(), e.getMessage()); 156 | stop(); 157 | } 158 | } 159 | 160 | private void logNewEvent(final Path watchedPath, final Kind eventKind, final Path target) { 161 | log.debug("Watched Resource changed, modified file [{}]", target.getFileName() 162 | .toString()); 163 | log.debug(" Event Kind [{}]", eventKind); 164 | log.debug(" Target [{}]", target); 165 | log.debug("Watched Path [{}]", watchedPath); 166 | } 167 | 168 | private Path path(final Object object) { 169 | return (Path) object; 170 | } 171 | 172 | private boolean isValidTargetFile(final Path target) { 173 | for (final Resource resource : this.resources) { 174 | if (pathMatchesResource(target, resource)) { 175 | return true; 176 | } 177 | } 178 | return false; 179 | } 180 | 181 | public Resource getResource(final Path target) { 182 | for (final Resource resource : this.resources) { 183 | if (pathMatchesResource(target, resource)) { 184 | return resource; 185 | } 186 | } 187 | return null; 188 | } 189 | 190 | private boolean pathMatchesResource(final Path target, final Resource resource) { 191 | return target.getFileName() 192 | .toString() 193 | .equals(resource.getFilename()); 194 | } 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/internal/ReadablePropertySourcesPlaceholderConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | import java.util.concurrent.Executors; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.BeanInitializationException; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 12 | import org.springframework.core.io.Resource; 13 | import org.springframework.core.io.support.PropertiesLoaderUtils; 14 | 15 | import com.morgan.design.properties.bean.PropertyModifiedEvent; 16 | import com.morgan.design.properties.event.PropertyChangedEventNotifier; 17 | import com.morgan.design.properties.internal.PropertiesWatcher.EventPublisher; 18 | import com.morgan.design.properties.resolver.PropertyResolver; 19 | 20 | /** 21 | * Specialisation of {@link PropertySourcesPlaceholderConfigurer} that can react to changes in the resources specified. The watching process does not start by 22 | * default, initiation is triggered by calling ReadablePropertySourcesPlaceholderConfigurer.startWatching() 23 | * 24 | * @author James Morgan 25 | */ 26 | public class ReadablePropertySourcesPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer implements EventPublisher { 27 | 28 | protected static Logger log = LoggerFactory.getLogger(ReadablePropertySourcesPlaceholderConfigurer.class); 29 | 30 | private final PropertyChangedEventNotifier eventNotifier; 31 | private final PropertyResolver propertyResolver; 32 | 33 | private Properties properties; 34 | private Resource[] locations; 35 | 36 | @Autowired 37 | public ReadablePropertySourcesPlaceholderConfigurer(final PropertyChangedEventNotifier eventNotifier, final PropertyResolver propertyResolver) { 38 | this.eventNotifier = eventNotifier; 39 | this.propertyResolver = propertyResolver; 40 | } 41 | 42 | @Override 43 | protected void loadProperties(final Properties props) throws IOException { 44 | super.loadProperties(props); 45 | this.properties = props; 46 | } 47 | 48 | @Override 49 | public void setLocations(final Resource[] locations) { 50 | super.setLocations(locations); 51 | this.locations = locations; 52 | } 53 | 54 | @Override 55 | public void onResourceChanged(final Resource resource) { 56 | try { 57 | final Properties reloadedProperties = PropertiesLoaderUtils.loadProperties(resource); 58 | for (final String property : this.properties.stringPropertyNames()) { 59 | 60 | final String oldValue = this.properties.getProperty(property); 61 | final String newValue = reloadedProperties.getProperty(property); 62 | 63 | if (propertyExistsAndNotNull(property, newValue) && propertyChange(oldValue, newValue)) { 64 | 65 | // Update locally stored copy of properties 66 | this.properties.setProperty(property, newValue); 67 | 68 | // Post change event to notify any potential listeners 69 | this.eventNotifier.post(new PropertyModifiedEvent(property, oldValue, newValue)); 70 | } 71 | } 72 | } 73 | catch (final IOException e) { 74 | log.error("Failed to reload properties file once change", e); 75 | } 76 | } 77 | 78 | public Properties getProperties() { 79 | return this.properties; 80 | } 81 | 82 | public void startWatching() { 83 | if (null == this.eventNotifier) { 84 | throw new BeanInitializationException("Event bus not setup, you should not be calling this method...!"); 85 | } 86 | try { 87 | // Here we actually create and set a FileWatcher to monitor the given locations 88 | Executors.newSingleThreadExecutor() 89 | .execute(new PropertiesWatcher(this.locations, this)); 90 | } 91 | catch (final IOException e) { 92 | log.error("Unable to start properties file watcher", e); 93 | } 94 | } 95 | 96 | public Object resolveProperty(final Object property) { 97 | Object resolvedPropertyValue = this.properties.get(this.propertyResolver.resolveProperty(property)); 98 | if (notStringpropertyToSubstitute(resolvedPropertyValue)) { 99 | return resolvedPropertyValue; 100 | } 101 | while (this.propertyResolver.requiresFurtherResoltuion(resolvedPropertyValue)) { 102 | resolvedPropertyValue = buildResolvedString(resolvedPropertyValue); 103 | } 104 | return resolvedPropertyValue; 105 | } 106 | 107 | private Object buildResolvedString(final Object resolvedPropertyValue) { 108 | final String resolvedValueStr = resolvedPropertyValue.toString(); 109 | 110 | final int startingIndex = resolvedValueStr.indexOf("${"); 111 | final int endingIndex = resolvedValueStr.indexOf("}", startingIndex) + 1; 112 | 113 | final String toResolve = resolvedValueStr.substring(startingIndex, endingIndex); 114 | final String resolved = resolveProperty(toResolve).toString(); 115 | 116 | return new StringBuilder() 117 | .append(resolvedValueStr.substring(0, startingIndex)) 118 | .append(resolved) 119 | .append(resolvedValueStr.substring(endingIndex)) 120 | .toString(); 121 | } 122 | 123 | private boolean notStringpropertyToSubstitute(final Object resolvedPropertyValue) { 124 | return !(resolvedPropertyValue instanceof String); 125 | } 126 | 127 | private boolean propertyChange(final String oldValue, final String newValue) { 128 | return null == oldValue || !oldValue.equals(newValue); 129 | } 130 | 131 | private boolean propertyExistsAndNotNull(final String property, final String newValue) { 132 | return this.properties.containsKey(property) && null != newValue; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/internal/ReloadablePropertyPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Modifier; 5 | import java.util.HashSet; 6 | import java.util.Map; 7 | import java.util.Properties; 8 | import java.util.Set; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.BeansException; 15 | import org.springframework.beans.factory.BeanInitializationException; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.util.ReflectionUtils; 20 | 21 | import com.google.common.collect.Maps; 22 | import com.google.common.eventbus.Subscribe; 23 | import com.morgan.design.properties.ReloadableProperty; 24 | import com.morgan.design.properties.bean.BeanPropertyHolder; 25 | import com.morgan.design.properties.bean.PropertyModifiedEvent; 26 | import com.morgan.design.properties.conversion.PropertyConversionService; 27 | import com.morgan.design.properties.event.PropertyChangedEventNotifier; 28 | 29 | /** 30 | *

31 | * Processes beans on start up injecting field values marked with {@link ReloadableProperty} setting the associated annotated property value with properties 32 | * configured in a {@link ReadablePropertySourcesPlaceholderConfigurer}. 33 | *

34 | *

35 | * The processor also has the ability to reload/re-inject properties from the configured {@link ReadablePropertySourcesPlaceholderConfigurer} which are changed. 36 | * Once a property is reloaded the associated bean holding that value will have its property updated, no further bean operations are performed on the reloaded 37 | * bean. 38 | *

39 | *

40 | * The processor will also substitute any properties with values starting with "${" and ending with "}", none recursive. 41 | *

42 | * 43 | * @author James Morgan 44 | */ 45 | @Component 46 | public class ReloadablePropertyPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { 47 | 48 | protected static Logger log = LoggerFactory.getLogger(ReloadablePropertyPostProcessor.class); 49 | 50 | private final PropertyChangedEventNotifier eventNotifier; 51 | private final PropertyConversionService propertyConversionService; 52 | private final ReadablePropertySourcesPlaceholderConfigurer placeholderConfigurer; 53 | 54 | private Map> beanPropertySubscriptions = Maps.newHashMap(); 55 | 56 | @Autowired 57 | public ReloadablePropertyPostProcessor(final ReadablePropertySourcesPlaceholderConfigurer placeholderConfigurer, 58 | final PropertyChangedEventNotifier eventNotifier, final PropertyConversionService conversionService) { 59 | this.eventNotifier = eventNotifier; 60 | this.placeholderConfigurer = placeholderConfigurer; 61 | this.propertyConversionService = conversionService; 62 | } 63 | 64 | @PostConstruct 65 | protected void init() { 66 | log.info("Registering ReloadablePropertyProcessor for properties file changes"); 67 | registerPropertyReloader(); 68 | } 69 | 70 | /** 71 | * Utility method to unregister the class from receiving events about property files being changed. 72 | */ 73 | public final void unregisterPropertyReloader() { 74 | log.info("Unregistering ReloadablePropertyProcessor from property file changes"); 75 | this.eventNotifier.unregister(this); 76 | } 77 | 78 | /** 79 | * Utility method to register the class for receiving events about property files being changed, setting up bean re-injection once triggered. 80 | */ 81 | public final void registerPropertyReloader() { 82 | // Setup Guava event bus listener 83 | this.eventNotifier.register(this); 84 | // Trigger resource change listener 85 | this.placeholderConfigurer.startWatching(); 86 | } 87 | 88 | /** 89 | * Method subscribing to the {@link PropertyModifiedEvent} utilising the {@link Subscribe} annotation 90 | * 91 | * @param event the {@link PropertyModifiedEvent} detailing what's changed 92 | */ 93 | @Subscribe 94 | public void handlePropertyChange(final PropertyModifiedEvent event) { 95 | for (final BeanPropertyHolder bean : this.beanPropertySubscriptions.get(event.getPropertyName())) { 96 | updateField(bean, event); 97 | } 98 | } 99 | 100 | public void updateField(final BeanPropertyHolder holder, final PropertyModifiedEvent event) { 101 | final Object beanToUpdate = holder.getBean(); 102 | final Field fieldToUpdate = holder.getField(); 103 | final String canonicalName = beanToUpdate.getClass() 104 | .getCanonicalName(); 105 | 106 | final Object convertedProperty = convertPropertyForField(fieldToUpdate, event.getPropertyName()); 107 | try { 108 | log.info("Reloading property [{}] on field [{}] for class [{}]", new Object[] { event.getPropertyName(), fieldToUpdate.getName(), canonicalName }); 109 | fieldToUpdate.set(beanToUpdate, convertedProperty); 110 | } 111 | catch (final IllegalAccessException e) { 112 | log.error("Unable to reloading property [{}] on field [{}] for class [{}]\n Exception [{}]", 113 | new Object[] { event.getPropertyName(), fieldToUpdate.getName(), canonicalName, e.getMessage() }); 114 | } 115 | } 116 | 117 | @Override 118 | public boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException { 119 | if (log.isDebugEnabled()) { 120 | log.debug("Setting Reloadable Properties on [{}]", beanName); 121 | } 122 | setPropertiesOnBean(bean); 123 | return true; 124 | } 125 | 126 | private void setPropertiesOnBean(final Object bean) { 127 | ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() { 128 | 129 | @Override 130 | public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException { 131 | 132 | final ReloadableProperty annotation = field.getAnnotation(ReloadableProperty.class); 133 | if (null != annotation) { 134 | 135 | ReflectionUtils.makeAccessible(field); 136 | validateFieldNotFinal(bean, field); 137 | 138 | final Object property = getProperties().get(annotation.value()); 139 | validatePropertyAvailableOrDefaultSet(bean, field, annotation, property); 140 | 141 | if (null != property) { 142 | 143 | log.info("Attempting to convert and set property [{}] on field [{}] for class [{}] to type [{}]", 144 | new Object[] { property, field.getName(), bean.getClass() 145 | .getCanonicalName(), field.getType() }); 146 | 147 | final Object convertedProperty = convertPropertyForField(field, annotation.value()); 148 | 149 | log.info("Setting field [{}] of class [{}] with value [{}]", new Object[] { field.getName(), bean.getClass() 150 | .getCanonicalName(), convertedProperty }); 151 | 152 | field.set(bean, convertedProperty); 153 | 154 | subscribeBeanToPropertyChangedEvent(annotation.value(), new BeanPropertyHolder(bean, field)); 155 | } 156 | else { 157 | log.info("Leaving field [{}] of class [{}] with default value", new Object[] { field.getName(), bean.getClass() 158 | .getCanonicalName() }); 159 | } 160 | } 161 | } 162 | }); 163 | } 164 | 165 | private void validatePropertyAvailableOrDefaultSet(final Object bean, final Field field, final ReloadableProperty annotation, final Object property) 166 | throws IllegalArgumentException, IllegalAccessException { 167 | if (null == property && fieldDoesNotHaveDefault(field, bean)) { 168 | throw new BeanInitializationException(String.format("No property found for field annotated with @ReloadableProperty, " 169 | + "and no default specified. Property [%s] of class [%s] requires a property named [%s]", field.getName(), bean.getClass() 170 | .getCanonicalName(), annotation.value())); 171 | } 172 | } 173 | 174 | private void validateFieldNotFinal(final Object bean, final Field field) { 175 | if (Modifier.isFinal(field.getModifiers())) { 176 | throw new BeanInitializationException(String.format("Unable to set field [%s] of class [%s] as is declared final", field.getName(), bean.getClass() 177 | .getCanonicalName())); 178 | } 179 | } 180 | 181 | private boolean fieldDoesNotHaveDefault(final Field field, final Object value) throws IllegalArgumentException, IllegalAccessException { 182 | try { 183 | return (null == field.get(value)); 184 | } 185 | catch (final NullPointerException e) { 186 | return true; 187 | } 188 | } 189 | 190 | private void subscribeBeanToPropertyChangedEvent(final String property, final BeanPropertyHolder fieldProperty) { 191 | if (!this.beanPropertySubscriptions.containsKey(property)) { 192 | this.beanPropertySubscriptions.put(property, new HashSet()); 193 | } 194 | this.beanPropertySubscriptions.get(property) 195 | .add(fieldProperty); 196 | } 197 | 198 | // /////////////////////////////////// 199 | // Utility methods for class access // 200 | // /////////////////////////////////// 201 | 202 | private Object convertPropertyForField(final Field field, final Object property) { 203 | return this.propertyConversionService.convertPropertyForField(field, resolverProperty(property)); 204 | } 205 | 206 | private Object resolverProperty(final Object property) { 207 | return this.placeholderConfigurer.resolveProperty(property); 208 | } 209 | 210 | private Properties getProperties() { 211 | return this.placeholderConfigurer.getProperties(); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/resolver/PropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.resolver; 2 | 3 | /** 4 | * Interface to be apply any special property resolution techniques on the given object, see {@link SubstitutingPropertyResolver} for default implementation 5 | * 6 | * @author James Morgan 7 | */ 8 | public interface PropertyResolver { 9 | 10 | /** 11 | * @param property The property to resolve by substitution, if required 12 | * @return The result of the property resolution, or the property itself if no substitution was required 13 | */ 14 | String resolveProperty(final Object property); 15 | 16 | /** 17 | * Can be used to check whether a property requires further resolution 18 | * 19 | * @param property The property to resolve by substitution, if required 20 | * @return true if the chosen {@link PropertyResolver} performs custom resolution 21 | */ 22 | boolean requiresFurtherResoltuion(final Object property); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/properties/resolver/SubstitutingPropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.resolver; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Implementation of a {@link PropertyResolver} resolving property keys via substitution. Substitution occurs only for properly formatted markers e.g 9 | * ${...} 10 | * 11 | *
12 |  * project.property 		= PropertyValue
13 |  * project.property.substitue = ${project.property}
14 |  * 
15 | * 16 | * @author James Morgan 17 | */ 18 | @Component 19 | public class SubstitutingPropertyResolver implements PropertyResolver { 20 | 21 | protected static Logger log = LoggerFactory.getLogger(SubstitutingPropertyResolver.class); 22 | 23 | @Override 24 | public String resolveProperty(final Object property) { 25 | final String stringProperty = property.toString(); 26 | 27 | // if property is a ${} then substitute it for the property it refers to 28 | final String resolvedProperty = propertyRequiresSubstitution(stringProperty) 29 | ? stringProperty.substring(2, stringProperty.length() - 1) 30 | : stringProperty; 31 | 32 | log.info("Property Resolved from [{}] to [{}]", new Object[] { property, resolvedProperty }); 33 | return resolvedProperty; 34 | } 35 | 36 | @Override 37 | public boolean requiresFurtherResoltuion(final Object property) { 38 | if (null == property) { 39 | log.info("Property is null"); 40 | return false; 41 | } 42 | final boolean propertyRequiresSubstitution = propertyRequiresSubstitution(property.toString()); 43 | if (propertyRequiresSubstitution) { 44 | log.info("Further resolution required for property value [{}]", new Object[] { property }); 45 | } 46 | return propertyRequiresSubstitution; 47 | } 48 | 49 | /** 50 | * Tests whether the given property is a ${...} property and therefore requires further resolution 51 | */ 52 | private boolean propertyRequiresSubstitution(final String property) { 53 | if (null != property) { 54 | return property.contains("${") && property.contains("}") && property.indexOf("${") < property.indexOf("}"); 55 | } 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/morgan/design/util/JodaUtils.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.util; 2 | 3 | import org.joda.time.LocalDate; 4 | import org.joda.time.LocalDateTime; 5 | import org.joda.time.LocalTime; 6 | import org.joda.time.Period; 7 | import org.joda.time.format.DateTimeFormat; 8 | import org.joda.time.format.DateTimeFormatter; 9 | import org.joda.time.format.PeriodFormatter; 10 | import org.joda.time.format.PeriodFormatterBuilder; 11 | 12 | import com.google.common.base.Strings; 13 | 14 | public class JodaUtils { 15 | 16 | public static final int MYSQL_SUNDAY = 0; 17 | 18 | public static final LocalTime END_OF_DAY = new LocalTime(23, 59, 59, 0); 19 | 20 | public static final LocalTime START_OF_DAY = new LocalTime(0, 0, 0, 0); 21 | 22 | private static final DateTimeFormatter DATE_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd"); 23 | 24 | private static final PeriodFormatter DEFAULT_PERIOD_FORMATTER = new PeriodFormatterBuilder().printZeroAlways() 25 | .minimumPrintedDigits(2) 26 | .appendHours() 27 | .appendSeparator(":") 28 | .appendMinutes() 29 | .appendSeparator(":") 30 | .appendSeconds() 31 | .toFormatter(); 32 | 33 | private static final DateTimeFormatter DEFAULT_TIME_FORMATTER = DateTimeFormat.forPattern("HH:mm:ss"); 34 | 35 | private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); 36 | 37 | private static final DateTimeFormatter TIMESTAMP_FORMAT_WITH_TRAILING_ZERO = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.0"); 38 | 39 | private JodaUtils() { 40 | throw new IllegalStateException("Constructor is private"); 41 | } 42 | 43 | /** 44 | * Returns a {@link LocalDate} representation of the given {@link String}. 45 | * 46 | * @param dateString The date as a {@link String} in the form "yyyy-MM-dd" 47 | * @param defaultValue The value to return if invoked with null 48 | * @return new {@link LocalDate} representation of the dateString 49 | */ 50 | public static LocalDate dateStringToLocalDateOrDefaultValue(final String dateString, final LocalDate defaultValue) { 51 | return Strings.isNullOrEmpty(dateString) 52 | ? defaultValue 53 | : new LocalDate(DATE_FORMAT.parseMillis(dateString)); 54 | } 55 | 56 | /** 57 | * Returns a {@link LocalDate} representation of the given {@link String}. 58 | * 59 | * @param dateString The date as a {@link String} in the form "yyyy-MM-dd" 60 | * @return new {@link LocalDate} representation of the dateString 61 | */ 62 | public static LocalDate dateStringToLocalDateOrNull(final String dateString) { 63 | return dateStringToLocalDateOrDefaultValue(dateString, null); 64 | } 65 | 66 | /** 67 | * Converts the given {@link String} in MySQL datetime format to a {@link LocalDateTime}. 68 | * 69 | * @param timestampString The date and time in "yyyy-MM-dd HH:mm:ss" format 70 | * @param defaultValue The value to return if the timestamp is null 71 | * @return A {@link LocalDateTime} representation of the given {@link String} 72 | * @throws IllegalArgumentException when given an invalid timestamp 73 | */ 74 | public static LocalDateTime timestampStringToLocalDateTimeOrDefaultValue(final String timestampString, final LocalDateTime defaultValue) { 75 | if (Strings.isNullOrEmpty(timestampString)) { 76 | return defaultValue; 77 | } 78 | return (timestampString.endsWith(".0")) 79 | ? new LocalDateTime(TIMESTAMP_FORMAT_WITH_TRAILING_ZERO.parseMillis(timestampString)) 80 | : new LocalDateTime(TIMESTAMP_FORMAT.parseMillis(timestampString)); 81 | } 82 | 83 | /** 84 | * Converts the given {@link String} in MySQL datetime format to a {@link LocalDateTime}. 85 | * 86 | * @param timestampString The date and time in "yyyy-MM-dd HH:mm:ss" format 87 | * @return A {@link LocalDateTime} representation of the given {@link String} 88 | * @throws IllegalArgumentException when given an invalid timestamp 89 | */ 90 | public static LocalDateTime timestampStringToLocalDateTimeOrNull(final String timestampString) { 91 | return timestampStringToLocalDateTimeOrDefaultValue(timestampString, null); 92 | } 93 | 94 | /** 95 | * Converts the given {@link String} in time format to a {@link LocalTime}. 96 | * 97 | * @param timeString The date and time in "HH:mm:ss" format 98 | * @param defaultValue The value to return if the timestamp is null 99 | * @return A {@link LocalTime} representation of the given {@link String} 100 | * @throws IllegalArgumentException when given an invalid time string 101 | */ 102 | public static LocalTime timeStringToLocalTimeOrDefaultValue(final String timeString, final LocalTime defaultValue) { 103 | if (Strings.isNullOrEmpty(timeString)) { 104 | return defaultValue; 105 | } 106 | return DEFAULT_TIME_FORMATTER.parseDateTime(timeString) 107 | .toLocalTime(); 108 | } 109 | 110 | /** 111 | * Converts the given {@link String} in time format to a {@link LocalTime}. 112 | * 113 | * @param timeString The date and time in "HH:mm:ss" format 114 | * @return A {@link LocalTime} representation of the given {@link String} 115 | * @throws IllegalArgumentException when given an invalid time string 116 | */ 117 | public static LocalTime timeStringToLocalTimeOrNull(final String timeString) { 118 | return timeStringToLocalTimeOrDefaultValue(timeString, null); 119 | } 120 | 121 | /** 122 | * Converts a time string to a {@link Period}. 123 | * 124 | * @param input The time in HH:mm:ss format 125 | * @param defaultValue The value to use if the input is null or blank 126 | * @return The {@link Period} represented by the given {@link String} 127 | * @throws IllegalArgumentException if there is an error parsing the {@link String} 128 | */ 129 | public static Period timeStringToPeriodOrDefaultValue(final String input, final Period defaultValue) { 130 | return (Strings.isNullOrEmpty(input)) 131 | ? defaultValue 132 | : DEFAULT_PERIOD_FORMATTER.parsePeriod(input); 133 | } 134 | 135 | /** 136 | * Converts a time string to a {@link Period}. 137 | * 138 | * @param input The time in HH:mm:ss format 139 | * @return The {@link Period} represented by the given {@link String} 140 | * @throws IllegalArgumentException if there is an error parsing the {@link String} 141 | */ 142 | public static Period timeStringToPeriodOrNull(final String input) { 143 | return timeStringToPeriodOrDefaultValue(input, null); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/resources/app.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmorgan/ReloadablePropertiesAnnotation/abd54e87d94293044c84bf71a2bda573b2f742c5/src/main/resources/app.properties -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-defaultConfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-reloadableProperties.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | classpath*:META-INF/*.properties 27 | classpath:test.properties 28 | file:${CONFIG_DIR}/global.properties 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/conversion/DefaultPropertyConversionServiceUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.conversion; 2 | 3 | import static org.hamcrest.Matchers.is; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import java.text.ParseException; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | 10 | import org.joda.time.LocalDate; 11 | import org.joda.time.LocalDateTime; 12 | import org.joda.time.LocalTime; 13 | import org.joda.time.Period; 14 | import org.junit.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.test.context.ContextConfiguration; 17 | import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 18 | 19 | @ContextConfiguration(locations = {"classpath:/spring/spring-reloadablePropertyPostProcessorIntTest.xml"}) 20 | public class DefaultPropertyConversionServiceUnitTest extends AbstractJUnit4SpringContextTests { 21 | 22 | @Autowired 23 | private PropertyConversionService conversionService; 24 | 25 | @Test 26 | public void shouldConvertPeriodForPropertyForField() throws NoSuchFieldException, SecurityException { 27 | assertThat((Period) convertPropertyForField("period", "24:00:00"), is(new Period(24, 0, 0, 0))); 28 | assertThat((Period) convertPropertyForField("period", "48:00:00"), is(new Period(48, 0, 0, 0))); 29 | assertThat((Period) convertPropertyForField("period", "72:00:00"), is(new Period(72, 0, 0, 0))); 30 | } 31 | 32 | @Test 33 | public void shouldConvertLocalDateTimeForPropertyForField() throws NoSuchFieldException, SecurityException { 34 | LocalDateTime actual = (LocalDateTime) convertPropertyForField("localDateTime", "2006-05-27 16:03:34.0"); 35 | assertThat(actual, is(new LocalDateTime(2006, 5, 27, 16, 3, 34))); 36 | 37 | actual = (LocalDateTime) convertPropertyForField("localDateTime", "2007-8-1 3:2:4.0"); 38 | assertThat(actual, is(new LocalDateTime(2007, 8, 1, 3, 2, 4))); 39 | } 40 | 41 | @Test 42 | public void shouldConvertLocalDateForPropertyForField() throws NoSuchFieldException, SecurityException { 43 | final LocalDate date = (LocalDate) convertPropertyForField("localDate", "2007-08-02"); 44 | assertThat(date, is(new LocalDate(2007, 8, 2))); 45 | } 46 | 47 | @Test 48 | public void shouldConvertLocalTimeForPropertyForField() throws NoSuchFieldException, SecurityException { 49 | assertThat((LocalTime) convertPropertyForField("localTime", "09:30:51"), is(new LocalTime(9, 30, 51))); 50 | assertThat((LocalTime) convertPropertyForField("localTime", "23:18:41"), is(new LocalTime(23, 18, 41))); 51 | } 52 | 53 | @Test 54 | public void shouldConvertBooleanValue() throws NoSuchFieldException, SecurityException { 55 | assertThat((Boolean) convertPropertyForField("booleanValue", "true"), is(true)); 56 | } 57 | 58 | @Test 59 | public void shouldConvertDateValue() throws NoSuchFieldException, SecurityException, ParseException { 60 | Date expected = new SimpleDateFormat("dd-MM-yyyy").parse("9-4-2017"); 61 | assertThat((Date) convertPropertyForField("dateValue", "9-4-2017"), is(expected)); 62 | } 63 | 64 | static class TestObject { 65 | Period period = new Period(); 66 | LocalTime localTime = new LocalTime(); 67 | LocalDate localDate = new LocalDate(); 68 | LocalDateTime localDateTime = new LocalDateTime(); 69 | Date dateValue = new Date(); 70 | boolean booleanValue; 71 | } 72 | 73 | private Object convertPropertyForField(final String fieldName, final Object value) throws NoSuchFieldException, SecurityException { 74 | return this.conversionService.convertPropertyForField(TestObject.class.getDeclaredField(fieldName), value); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/event/GuavaPropertyChangedEventNotifierUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.event; 2 | 3 | import org.jmock.Expectations; 4 | import org.jmock.Mockery; 5 | import org.jmock.integration.junit4.JUnit4Mockery; 6 | import org.jmock.integration.junit4.JUnitRuleMockery; 7 | import org.jmock.lib.legacy.ClassImposteriser; 8 | import org.junit.Before; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | 12 | import com.google.common.eventbus.EventBus; 13 | import com.morgan.design.properties.bean.PropertyModifiedEvent; 14 | import com.morgan.design.properties.internal.ReloadablePropertyPostProcessor; 15 | 16 | @SuppressWarnings("unqualified-field-access") 17 | public class GuavaPropertyChangedEventNotifierUnitTest { 18 | 19 | @Rule 20 | public JUnitRuleMockery ruleMockery = new JUnitRuleMockery(); 21 | 22 | Mockery context = new JUnit4Mockery() { 23 | { 24 | setImposteriser(ClassImposteriser.INSTANCE); 25 | } 26 | }; 27 | 28 | private GuavaPropertyChangedEventNotifier eventNotifier; 29 | 30 | private EventBus eventBus; 31 | 32 | @Before 33 | public void setUp() throws Exception { 34 | this.eventBus = this.context.mock(EventBus.class); 35 | this.eventNotifier = new GuavaPropertyChangedEventNotifier(this.eventBus); 36 | } 37 | 38 | @Test 39 | public void shouldPostWhenEventFound() { 40 | final PropertyModifiedEvent event = new PropertyModifiedEvent("", new Object(), new Object()); 41 | this.context.checking(new Expectations() { 42 | { 43 | oneOf(eventBus).post(event); 44 | } 45 | }); 46 | this.eventNotifier.post(event); 47 | } 48 | 49 | @Test 50 | public void shouldRegisterNewRegistery() { 51 | final ReloadablePropertyPostProcessor registery = new ReloadablePropertyPostProcessor(null, null, null); 52 | this.context.checking(new Expectations() { 53 | { 54 | oneOf(eventBus).register(registery); 55 | } 56 | }); 57 | this.eventNotifier.register(registery); 58 | } 59 | 60 | @Test 61 | public void shouldUnregisterRegistery() { 62 | final ReloadablePropertyPostProcessor registery = new ReloadablePropertyPostProcessor(null, null, null); 63 | this.context.checking(new Expectations() { 64 | { 65 | oneOf(eventBus).unregister(registery); 66 | } 67 | }); 68 | this.eventNotifier.unregister(registery); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/internal/FailingReloadablePropertyPostProcessorIntTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.hamcrest.Matchers.instanceOf; 5 | import static org.hamcrest.Matchers.is; 6 | import static org.junit.Assert.assertThat; 7 | import static org.junit.Assert.fail; 8 | 9 | import org.junit.Test; 10 | import org.springframework.beans.factory.BeanCreationException; 11 | import org.springframework.beans.factory.BeanInitializationException; 12 | import org.springframework.context.support.ClassPathXmlApplicationContext; 13 | 14 | public class FailingReloadablePropertyPostProcessorIntTest { 15 | 16 | @Test 17 | public void shouldThrowBeanInitializationExceptionWhenNoPropertyFoundAndNoDefaultValue() { 18 | try { 19 | new ClassPathXmlApplicationContext("classpath:/spring/spring-missingProperty.xml"); 20 | fail("Should have thrown BeanException due to missing property"); 21 | } 22 | catch (final BeanCreationException e) { 23 | assertThat(e.getCause(), is(instanceOf(BeanInitializationException.class))); 24 | assertThat(e.getCause() 25 | .getMessage(), containsString("requires a property named [does.not.exist]")); 26 | } 27 | } 28 | 29 | @Test 30 | public void shouldThrowBeanInitializationExceptionWhenUnableToConvertPropertyToFieldType() { 31 | try { 32 | new ClassPathXmlApplicationContext("classpath:/spring/spring-badValue.xml"); 33 | fail("Should have thrown BeanException due to bad value for conversion"); 34 | } 35 | catch (final BeanCreationException e) { 36 | assertThat(e.getCause(), is(instanceOf(BeanInitializationException.class))); 37 | assertThat(e.getCause() 38 | .getMessage(), containsString("Unable to convert property")); 39 | } 40 | } 41 | 42 | @Test 43 | public void shouldThrowBeanInitializationExceptionWhenFieldSetAsFinal() { 44 | try { 45 | new ClassPathXmlApplicationContext("classpath:/spring/spring-finalFieldBean.xml"); 46 | fail("Should have thrown BeanException due to bad value for conversion"); 47 | } 48 | catch (final BeanCreationException e) { 49 | assertThat(e.getCause(), is(instanceOf(BeanInitializationException.class))); 50 | assertThat(e.getCause() 51 | .getMessage(), containsString("Unable to set field")); 52 | assertThat(e.getCause() 53 | .getMessage(), containsString("as is declared final")); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/internal/PropertiesWatcherUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertNull; 5 | import static org.hamcrest.CoreMatchers.is; 6 | import static org.junit.Assert.assertThat; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import org.junit.Test; 15 | import org.springframework.core.io.FileSystemResource; 16 | import org.springframework.core.io.Resource; 17 | 18 | import com.google.common.base.Charsets; 19 | import com.google.common.io.Files; 20 | import com.morgan.design.properties.internal.PropertiesWatcher.EventPublisher; 21 | 22 | public class PropertiesWatcherUnitTest { 23 | 24 | private static final int _1_SEC = 1000; 25 | private static final int _2_SEC = 2000; 26 | 27 | private Resource actual; 28 | private CountDownLatch lock = new CountDownLatch(0); 29 | 30 | final File propertiesFile1 = new File("target/test-classes/test-files/fileWatcher.properties"); 31 | final File propertiesFile2 = new File("target/test-classes/test-files/different_fileWatcher.properties"); 32 | final File propertiesFile3 = new File("target/test-classes/test-files2/fileWatcher2.properties"); 33 | 34 | final Resource[] singleResource = new Resource[] { new FileSystemResource(this.propertiesFile1) }; 35 | final Resource[] multiResourceSameDir = new Resource[] { new FileSystemResource(this.propertiesFile1), new FileSystemResource(this.propertiesFile2) }; 36 | final Resource[] multiResourceDifferentDir = new Resource[] { new FileSystemResource(this.propertiesFile1), new FileSystemResource(this.propertiesFile3) }; 37 | 38 | @Test 39 | public final void testWatchingASingle() throws IOException, InterruptedException { 40 | resetCountDownLatch(this.singleResource.length); 41 | 42 | final PropertiesWatcher propertiesWatcher = createPropertiesWatcher(this.singleResource); 43 | confirmTestDataNotSet(); 44 | 45 | startPropertiesWatcher(propertiesWatcher); 46 | wait(_1_SEC); 47 | 48 | modifyPropertiesFile(this.propertiesFile1); 49 | wait(_2_SEC); 50 | confirmPropertiesFileModified(this.propertiesFile1); 51 | 52 | propertiesWatcher.stop(); 53 | wait(_2_SEC); 54 | } 55 | 56 | @Test 57 | public final void testWatchingMulitipleResourcesInDifferingDirectories() throws IOException, InterruptedException { 58 | resetCountDownLatch(this.multiResourceDifferentDir.length); 59 | 60 | final PropertiesWatcher propertiesWatcher = createPropertiesWatcher(this.multiResourceDifferentDir); 61 | confirmTestDataNotSet(); 62 | 63 | startPropertiesWatcher(propertiesWatcher); 64 | wait(_1_SEC); 65 | 66 | modifyPropertiesFile(this.propertiesFile3); 67 | wait(_2_SEC); 68 | confirmPropertiesFileModified(this.propertiesFile3); 69 | 70 | resetTestData(); 71 | confirmTestDataNotSet(); 72 | 73 | modifyPropertiesFile(this.propertiesFile1); 74 | wait(_2_SEC); 75 | confirmPropertiesFileModified(this.propertiesFile1); 76 | 77 | propertiesWatcher.stop(); 78 | wait(_1_SEC); 79 | } 80 | 81 | @Test 82 | public final void testModifyingADifferentResoruceInSameDirectory() throws IOException, InterruptedException { 83 | resetCountDownLatch(this.multiResourceSameDir.length); 84 | 85 | final PropertiesWatcher propertiesWatcher = createPropertiesWatcher(this.multiResourceSameDir); 86 | confirmTestDataNotSet(); 87 | 88 | startPropertiesWatcher(propertiesWatcher); 89 | wait(_1_SEC); 90 | 91 | modifyPropertiesFile(this.propertiesFile1); 92 | wait(_2_SEC); 93 | confirmPropertiesFileModified(this.propertiesFile1); 94 | 95 | resetTestData(); 96 | wait(_1_SEC); 97 | 98 | confirmTestDataNotSet(); 99 | wait(_1_SEC); 100 | 101 | modifyPropertiesFile(this.propertiesFile2); 102 | wait(_2_SEC); 103 | confirmPropertiesFileModified(this.propertiesFile2); 104 | 105 | propertiesWatcher.stop(); 106 | wait(_1_SEC); 107 | } 108 | 109 | private void resetCountDownLatch(final int count) { 110 | this.lock = new CountDownLatch(count); 111 | } 112 | 113 | private void startPropertiesWatcher(final PropertiesWatcher propertiesWatcher) { 114 | Executors.newSingleThreadExecutor() 115 | .execute(propertiesWatcher); 116 | } 117 | 118 | private void resetTestData() { 119 | this.actual = null; 120 | } 121 | 122 | private void confirmTestDataNotSet() { 123 | assertNull(this.actual); 124 | } 125 | 126 | private void confirmPropertiesFileModified(final File file) { 127 | assertNotNull(this.actual); 128 | assertThat(this.actual.getFilename(), is(file.getName())); 129 | } 130 | 131 | private void wait(final int millis) throws InterruptedException { 132 | this.lock.await(millis, TimeUnit.MILLISECONDS); 133 | } 134 | 135 | private void modifyPropertiesFile(final File file) throws IOException { 136 | Files.write("random string", file, Charsets.UTF_8); 137 | } 138 | 139 | private PropertiesWatcher createPropertiesWatcher(final Resource[] resources) throws IOException { 140 | final PropertiesWatcher propertiesWatcher = new PropertiesWatcher(resources, new EventPublisher() { 141 | @Override 142 | @SuppressWarnings("unqualified-field-access") 143 | public void onResourceChanged(final Resource data) { 144 | actual = data; 145 | lock.countDown(); 146 | } 147 | }); 148 | return propertiesWatcher; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/internal/ReloadablePropertyPostProcessorIntTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import static org.hamcrest.Matchers.is; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import java.math.BigDecimal; 7 | import java.math.BigInteger; 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | import org.joda.time.LocalDate; 13 | import org.joda.time.LocalDateTime; 14 | import org.joda.time.LocalTime; 15 | import org.joda.time.Period; 16 | import org.junit.Test; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.test.context.ContextConfiguration; 19 | import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 20 | 21 | import com.morgan.design.properties.testBeans.AutowiredPropertyBean; 22 | 23 | @ContextConfiguration(locations = { "classpath:/spring/spring-reloadablePropertyPostProcessorIntTest.xml" }) 24 | public class ReloadablePropertyPostProcessorIntTest extends AbstractJUnit4SpringContextTests { 25 | 26 | @Autowired 27 | private AutowiredPropertyBean bean; 28 | 29 | @Test 30 | public void shouldRecurseThroughNestedPropertiesWhenAutowiring() { 31 | assertThat("Property was not resolved as expected", this.bean.getSubstitutedProperty(), is("elephant")); 32 | } 33 | 34 | @Test 35 | public void shouldNotAdjustFieldWhenNotAnnotatedWithAutowiredProperty() { 36 | assertThat("Field not annotated should not be changed", this.bean.getNotAnnotated(), is("Original value")); 37 | } 38 | 39 | @Test 40 | public void shouldPreserveDefaultIfNoPropertyReplacementFound() { 41 | assertThat("Should have preserved default value", this.bean.getWithDefaultValue(), is("Default Value")); 42 | assertThat("Should have preserved default value for primitive", this.bean.getPrimitiveWithDefaultValue(), is(55)); 43 | } 44 | 45 | @Test 46 | public void shouldInjectStringValue() { 47 | assertThat(this.bean.getStringProperty(), is("Injected String Value")); 48 | } 49 | 50 | @Test 51 | public void shouldInjectBooleanValue() { 52 | assertThat(this.bean.getBooleanProperty(), is(true)); 53 | } 54 | 55 | @Test 56 | public void shouldInjectIntValue() { 57 | assertThat(this.bean.getIntProperty(), is(42)); 58 | assertThat(this.bean.getIntObjectProperty(), is(42)); 59 | } 60 | 61 | @Test 62 | public void shouldInjectLongValue() { 63 | assertThat(this.bean.getLongProperty(), is(12345L)); 64 | assertThat(this.bean.getLongObjectProperty(), is(12345L)); 65 | } 66 | 67 | @Test 68 | public void shouldInjectDoubleValue() { 69 | assertThat(this.bean.getDoubleProperty(), is(12345.67)); 70 | assertThat(this.bean.getDoubleObjectProperty(), is(12345.67)); 71 | } 72 | 73 | @Test 74 | public void shouldInjectBigIntegerValue() { 75 | assertThat(this.bean.getBigIntegerProperty(), is(new BigInteger("224411"))); 76 | } 77 | 78 | @Test 79 | public void shouldInjectBigDecimalValue() { 80 | assertThat("Should have 2 decimal places", this.bean.getBigDecimalProperty() 81 | .scale(), is(2)); 82 | assertThat(this.bean.getBigDecimalProperty(), is(new BigDecimal("20012.56"))); 83 | } 84 | 85 | @Test 86 | public void shouldInjectPeriodValue() { 87 | assertThat(this.bean.getPeriodProperty(), is(new Period(0, 12, 22, 0))); 88 | } 89 | 90 | @Test 91 | public void shouldInjectLocalDateValue() { 92 | assertThat(this.bean.getLocalDateProperty(), is(new LocalDate(2009, 06, 12))); 93 | } 94 | 95 | @Test 96 | public void shouldInjectLocalDateTimeValue() { 97 | assertThat(this.bean.getLocalDateTimeProperty(), is(new LocalDateTime(2009, 7, 5, 12, 56, 2))); 98 | } 99 | 100 | @Test 101 | public void shouldInjectLocalTimeValue() { 102 | assertThat(this.bean.getLocalTimeProperty(), is(new LocalTime(12, 22, 45))); 103 | } 104 | 105 | @Test 106 | public void shouldInjectDateValue() throws ParseException { 107 | Date d = new SimpleDateFormat("dd-MM-yyyy").parse("14-4-2017"); // exception is never throw 108 | assertThat(this.bean.getDateProperty(), is(d)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/internal/UpdatingReloadablePropertyPostProcessorIntTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.internal; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | import java.nio.file.Files; 10 | import java.nio.file.OpenOption; 11 | import java.util.Properties; 12 | 13 | import org.junit.After; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.core.io.support.PropertiesLoaderUtils; 18 | import org.springframework.test.context.ContextConfiguration; 19 | import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 20 | 21 | import com.morgan.design.properties.testBeans.ReloadingAutowiredPropertyBean; 22 | 23 | @ContextConfiguration(locations = { "classpath:/spring/spring-reloading-reloadablePropertyPostProcessorIntTest.xml" }) 24 | public class UpdatingReloadablePropertyPostProcessorIntTest extends AbstractJUnit4SpringContextTests { 25 | 26 | private static final String DIR = "target/test-classes/test-files/"; 27 | private static final String PROPERTIES = "reloading.properties"; 28 | 29 | @Autowired 30 | private ReloadingAutowiredPropertyBean bean; 31 | 32 | private Properties loadedProperties; 33 | 34 | @Before 35 | public void setUp() throws IOException { 36 | this.loadedProperties = PropertiesLoaderUtils.loadAllProperties(PROPERTIES); 37 | assertThat(this.bean.getStringProperty(), is("Injected String Value")); 38 | assertThat(this.bean.getCompositeStringProperty(), is("Hello, World!")); 39 | } 40 | 41 | @After 42 | public void cleanUp() throws Exception { 43 | this.loadedProperties.setProperty("dynamicProperty.stringValue", "Injected String Value"); 44 | this.loadedProperties.setProperty("dynamicProperty.baseStringValue", "World"); 45 | this.loadedProperties.setProperty("dynamicProperty.compoiteStringValue", "Hello, ${dynamicProperty.baseStringValue}!"); 46 | 47 | final OutputStream newOutputStream = Files.newOutputStream(new File(DIR + PROPERTIES).toPath(), new OpenOption[] {}); 48 | this.loadedProperties.store(newOutputStream, null); 49 | 50 | Thread.sleep(500); // this is a hack -> I need to find an alternative 51 | 52 | assertThat(this.bean.getStringProperty(), is("Injected String Value")); 53 | assertThat(this.bean.getCompositeStringProperty(), is("Hello, World!")); 54 | } 55 | 56 | @Test 57 | public void shouldReloadAlteredStringProperty() throws Exception { 58 | assertThat(this.bean.getStringProperty(), is("Injected String Value")); 59 | 60 | this.loadedProperties.setProperty("dynamicProperty.stringValue", "Altered Injected String Value"); 61 | 62 | final File file = new File(DIR + PROPERTIES); 63 | final OutputStream newOutputStream = Files.newOutputStream(file.toPath(), new OpenOption[] {}); 64 | this.loadedProperties.store(newOutputStream, null); 65 | newOutputStream.flush(); 66 | newOutputStream.close(); 67 | 68 | Thread.sleep(500); // this is a hack -> I need to find an alternative 69 | 70 | assertThat(this.bean.getStringProperty(), is("Altered Injected String Value")); 71 | } 72 | 73 | @Test 74 | public void shouldReloadAlteredCompositeStringProperty() throws Exception { 75 | assertThat(this.bean.getCompositeStringProperty(), is("Hello, World!")); 76 | 77 | this.loadedProperties.setProperty("dynamicProperty.compoiteStringValue", "Goodbye, ${dynamicProperty.baseStringValue}!"); 78 | assertThat(this.loadedProperties.getProperty("dynamicProperty.compoiteStringValue"), is("Goodbye, ${dynamicProperty.baseStringValue}!")); 79 | 80 | final File file = new File(DIR + PROPERTIES); 81 | final OutputStream newOutputStream = Files.newOutputStream(file.toPath(), new OpenOption[] {}); 82 | this.loadedProperties.store(newOutputStream, null); 83 | newOutputStream.flush(); 84 | newOutputStream.close(); 85 | 86 | Thread.sleep(1000); // this is a hack -> I need to find an alternative 87 | 88 | assertThat(this.bean.getCompositeStringProperty(), is("Goodbye, World!")); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/resolver/SubstitutingPropertyResolverUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.resolver; 2 | 3 | import static org.hamcrest.Matchers.nullValue; 4 | import static org.hamcrest.core.Is.is; 5 | import static org.junit.Assert.assertThat; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import com.morgan.design.properties.resolver.SubstitutingPropertyResolver; 11 | 12 | public class SubstitutingPropertyResolverUnitTest { 13 | 14 | private SubstitutingPropertyResolver substitutingPropertyResolver; 15 | 16 | @Before 17 | public void setUp() throws Exception { 18 | this.substitutingPropertyResolver = new SubstitutingPropertyResolver(); 19 | } 20 | 21 | @Test 22 | public void testResolveProperty() { 23 | assertThat(this.substitutingPropertyResolver.resolveProperty(""), is("")); 24 | assertThat(this.substitutingPropertyResolver.resolveProperty("project.property"), is("project.property")); 25 | assertThat(this.substitutingPropertyResolver.resolveProperty("${project.property}"), is("project.property")); 26 | } 27 | 28 | @Test(expected = NullPointerException.class) 29 | public void expectedNPEWhenNullPropertyResolved() { 30 | assertThat(this.substitutingPropertyResolver.resolveProperty(null), is(nullValue())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/testBeans/AutowiredPropertyBean.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.testBeans; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.BigInteger; 5 | import java.util.Date; 6 | 7 | import org.joda.time.LocalDate; 8 | import org.joda.time.LocalDateTime; 9 | import org.joda.time.LocalTime; 10 | import org.joda.time.Period; 11 | import org.springframework.stereotype.Component; 12 | 13 | import com.morgan.design.properties.ReloadableProperty; 14 | 15 | @Component 16 | public class AutowiredPropertyBean { 17 | 18 | private String notAnnotated = "Original value"; 19 | 20 | @ReloadableProperty("not.in.the.file") 21 | private String withDefaultValue = "Default Value"; 22 | 23 | @ReloadableProperty("not.in.the.file") 24 | private int primitiveWithDefaultValue = 55; 25 | 26 | @ReloadableProperty("dynamicProperty.stringValue") 27 | private String stringProperty; 28 | 29 | // primitives 30 | 31 | @ReloadableProperty("dynamicProperty.booleanValue") 32 | private boolean booleanProperty; 33 | 34 | @ReloadableProperty("dynamicProperty.intValue") 35 | private int intProperty; 36 | 37 | @ReloadableProperty("dynamicProperty.longValue") 38 | private long longProperty; 39 | 40 | @ReloadableProperty("dynamicProperty.doubleValue") 41 | private double doubleProperty; 42 | 43 | // object wrappers 44 | 45 | @ReloadableProperty("dynamicProperty.intValue") 46 | private Integer intObjectProperty; 47 | 48 | @ReloadableProperty("dynamicProperty.longValue") 49 | private Long longObjectProperty; 50 | 51 | @ReloadableProperty("dynamicProperty.doubleValue") 52 | private Double doubleObjectProperty; 53 | 54 | // math 55 | 56 | @ReloadableProperty("dynamicProperty.bigIntegerValue") 57 | private BigInteger bigIntegerProperty; 58 | 59 | @ReloadableProperty("dynamicProperty.bigDecimalValue") 60 | private BigDecimal bigDecimalProperty; 61 | 62 | // joda 63 | 64 | @ReloadableProperty("dynamicProperty.periodValue") 65 | private Period periodProperty; 66 | 67 | @ReloadableProperty("dynamicProperty.localDateTimeValue") 68 | private LocalDateTime localDateTimeProperty; 69 | 70 | @ReloadableProperty("dynamicProperty.localDateValue") 71 | private LocalDate localDateProperty; 72 | 73 | @ReloadableProperty("dynamicProperty.localTimeValue") 74 | private LocalTime localTimeProperty; 75 | 76 | // java date 77 | @ReloadableProperty("dynamicProperty.dateValue") 78 | private Date dateProperty; 79 | 80 | // recursive substitution 81 | 82 | @ReloadableProperty("dynamicProperty.substitutionProperty") 83 | private String substitutedProperty; 84 | 85 | // getters/setters 86 | 87 | public String getNotAnnotated() { 88 | return this.notAnnotated; 89 | } 90 | 91 | public String getWithDefaultValue() { 92 | return this.withDefaultValue; 93 | } 94 | 95 | public int getPrimitiveWithDefaultValue() { 96 | return this.primitiveWithDefaultValue; 97 | } 98 | 99 | public String getStringProperty() { 100 | return this.stringProperty; 101 | } 102 | 103 | public boolean getBooleanProperty() { 104 | return this.booleanProperty; 105 | } 106 | 107 | public int getIntProperty() { 108 | return this.intProperty; 109 | } 110 | 111 | public long getLongProperty() { 112 | return this.longProperty; 113 | } 114 | 115 | public double getDoubleProperty() { 116 | return this.doubleProperty; 117 | } 118 | 119 | public Double getDoubleObjectProperty() { 120 | return this.doubleObjectProperty; 121 | } 122 | 123 | public Integer getIntObjectProperty() { 124 | return this.intObjectProperty; 125 | } 126 | 127 | public Long getLongObjectProperty() { 128 | return this.longObjectProperty; 129 | } 130 | 131 | public BigDecimal getBigDecimalProperty() { 132 | return this.bigDecimalProperty; 133 | } 134 | 135 | public BigInteger getBigIntegerProperty() { 136 | return this.bigIntegerProperty; 137 | } 138 | 139 | public Period getPeriodProperty() { 140 | return this.periodProperty; 141 | } 142 | 143 | public LocalDate getLocalDateProperty() { 144 | return this.localDateProperty; 145 | } 146 | 147 | public LocalTime getLocalTimeProperty() { 148 | return this.localTimeProperty; 149 | } 150 | 151 | public LocalDateTime getLocalDateTimeProperty() { 152 | return this.localDateTimeProperty; 153 | } 154 | 155 | public String getSubstitutedProperty() { 156 | return this.substitutedProperty; 157 | } 158 | 159 | public Date getDateProperty() { 160 | return this.dateProperty; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/testBeans/BadValue.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.testBeans; 2 | 3 | import org.joda.time.Period; 4 | import org.springframework.stereotype.Component; 5 | 6 | import com.morgan.design.properties.ReloadableProperty; 7 | 8 | @Component 9 | public class BadValue { 10 | 11 | @ReloadableProperty("invalid.period") 12 | private Period period; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/testBeans/FinalFieldBean.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.testBeans; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import com.morgan.design.properties.ReloadableProperty; 6 | 7 | @Component 8 | public class FinalFieldBean { 9 | 10 | @ReloadableProperty("dynamicProperty.intValue") 11 | private final Integer intObjectProperty = 999; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/testBeans/MissingProperty.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.testBeans; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import com.morgan.design.properties.ReloadableProperty; 6 | 7 | @Component 8 | public class MissingProperty { 9 | 10 | @ReloadableProperty("does.not.exist") 11 | private String hasNoDefaultValue; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/properties/testBeans/ReloadingAutowiredPropertyBean.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.properties.testBeans; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import com.morgan.design.properties.ReloadableProperty; 6 | 7 | @Component 8 | public class ReloadingAutowiredPropertyBean { 9 | 10 | @ReloadableProperty("dynamicProperty.stringValue") 11 | private String stringProperty; 12 | 13 | @ReloadableProperty("dynamicProperty.compoiteStringValue") 14 | private String compositeStringProperty; 15 | 16 | public String getStringProperty() { 17 | return this.stringProperty; 18 | } 19 | 20 | public String getCompositeStringProperty() { 21 | return this.compositeStringProperty; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/morgan/design/util/JodaUtilsUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.morgan.design.util; 2 | 3 | import static org.hamcrest.Matchers.is; 4 | import static org.hamcrest.Matchers.nullValue; 5 | import static org.junit.Assert.assertThat; 6 | 7 | import org.joda.time.DateTimeUtils; 8 | import org.joda.time.LocalDate; 9 | import org.joda.time.LocalDateTime; 10 | import org.joda.time.Period; 11 | import org.junit.After; 12 | import org.junit.Test; 13 | 14 | /** 15 | * @author James Morgan 16 | */ 17 | public class JodaUtilsUnitTest { 18 | 19 | @After 20 | public void tearDown() { 21 | DateTimeUtils.setCurrentMillisSystem(); 22 | } 23 | 24 | @Test 25 | public void shouldNotConvertDateStringToLocalDateForBlankDate() { 26 | assertThat(JodaUtils.dateStringToLocalDateOrNull(null), is(nullValue())); 27 | assertThat(JodaUtils.dateStringToLocalDateOrNull(""), is(nullValue())); 28 | } 29 | 30 | @Test(expected = IllegalArgumentException.class) 31 | public void shouldNotConvertDateStringToLocalDateForInvalidDate() { 32 | JodaUtils.dateStringToLocalDateOrNull("12:34:01"); 33 | } 34 | 35 | @Test 36 | public void shouldConvertDateStringToLocalDateForValidDate() { 37 | assertThat(JodaUtils.dateStringToLocalDateOrNull("2007-08-02"), is(new LocalDate(2007, 8, 2))); 38 | assertThat(JodaUtils.dateStringToLocalDateOrNull("2007-9-9"), is(new LocalDate(2007, 9, 9))); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void shouldThrowExceptionWhenConvertingInvalidTimestampStringToLocalDateTime() { 43 | JodaUtils.timestampStringToLocalDateTimeOrNull("2007-13-40 25:61:61"); 44 | } 45 | 46 | @Test(expected = IllegalArgumentException.class) 47 | public void shouldThrowExceptionWhenConvertingNonTimestampStringToLocalDateTime() { 48 | JodaUtils.timestampStringToLocalDateTimeOrNull("balderdash"); 49 | } 50 | 51 | @Test 52 | public void canConvertTimestampStringToLocalDateTimeWithExtraZero() { 53 | assertThat("The method should return null when invoked with null", JodaUtils.timestampStringToLocalDateTimeOrNull(null), is(nullValue())); 54 | 55 | LocalDateTime actual = JodaUtils.timestampStringToLocalDateTimeOrNull("2006-05-27 16:03:34.0"); 56 | assertThat("The expected outcome was not returned", actual, is(new LocalDateTime(2006, 5, 27, 16, 3, 34))); 57 | 58 | actual = JodaUtils.timestampStringToLocalDateTimeOrNull("2007-8-1 3:2:4.0"); 59 | assertThat("The expected outcome was not returned", actual, is(new LocalDateTime(2007, 8, 1, 3, 2, 4))); 60 | } 61 | 62 | @Test 63 | public void shouldConvertTimeStringIntoPeriod() { 64 | assertThat(JodaUtils.timeStringToPeriodOrNull("24:00:00"), is(new Period(24, 0, 0, 0))); 65 | assertThat(JodaUtils.timeStringToPeriodOrNull("48:00:00"), is(new Period(48, 0, 0, 0))); 66 | assertThat(JodaUtils.timeStringToPeriodOrNull("72:00:00"), is(new Period(72, 0, 0, 0))); 67 | 68 | assertThat(JodaUtils.timeStringToPeriodOrNull("24:30:00"), is(new Period(24, 30, 0, 0))); 69 | assertThat(JodaUtils.timeStringToPeriodOrNull("48:30:00"), is(new Period(48, 30, 0, 0))); 70 | assertThat(JodaUtils.timeStringToPeriodOrNull("72:30:00"), is(new Period(72, 30, 0, 0))); 71 | 72 | assertThat(JodaUtils.timeStringToPeriodOrNull("24:30:15"), is(new Period(24, 30, 15, 0))); 73 | assertThat(JodaUtils.timeStringToPeriodOrNull("48:30:15"), is(new Period(48, 30, 15, 0))); 74 | assertThat(JodaUtils.timeStringToPeriodOrNull("72:30:15"), is(new Period(72, 30, 15, 0))); 75 | 76 | assertThat(JodaUtils.timeStringToPeriodOrNull("124:30:00"), is(new Period(124, 30, 0, 0))); 77 | assertThat(JodaUtils.timeStringToPeriodOrNull("00:00:00"), is(new Period(0, 0, 0, 0))); 78 | 79 | assertThat(JodaUtils.timeStringToPeriodOrNull(""), is(nullValue())); 80 | assertThat(JodaUtils.timeStringToPeriodOrNull(null), is(nullValue())); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/spring/spring-badValue.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | classpath*:test-files/example.properties 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/resources/spring/spring-finalFieldBean.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | classpath*:test-files/example.properties 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/resources/spring/spring-missingProperty.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | classpath*:test-files/example.properties 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/resources/spring/spring-reloadablePropertyPostProcessorIntTest.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | classpath*:test-files/example.properties 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/resources/spring/spring-reloading-reloadablePropertyPostProcessorIntTest.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | classpath*:test-files/reloading.properties 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/resources/test-files/different_fileWatcher.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 15 12:18:45 BST 2012 2 | dynamicProperty.longValue=12345 3 | dynamicProperty.substitutionProperty=${dynamicProperty.substitutionValue} 4 | dynamicProperty.doubleValue=12345.67 5 | dynamicProperty.localTimeValue=12:22:45 6 | dynamicProperty.stringValue=Injected String Value 7 | dynamicProperty.substitutionValue=elephant 8 | dynamicProperty.bigDecimalValue=20012.56 9 | dynamicProperty.intValue=42 10 | dynamicProperty.localDateValue=2009-06-12 11 | dynamicProperty.periodValue=00:12:22 12 | dynamicProperty.booleanValue=true 13 | dynamicProperty.localDateTimeValue=2009-07-05 12:56:02 14 | invalid.period=12:22: 15 | dynamicProperty.bigIntegerValue=224411 16 | -------------------------------------------------------------------------------- /src/test/resources/test-files/example.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 15 12:18:45 BST 2012 2 | dynamicProperty.longValue=12345 3 | dynamicProperty.substitutionProperty=${dynamicProperty.substitutionValue} 4 | dynamicProperty.doubleValue=12345.67 5 | dynamicProperty.localTimeValue=12:22:45 6 | dynamicProperty.stringValue=Injected String Value 7 | dynamicProperty.substitutionValue=elephant 8 | dynamicProperty.bigDecimalValue=20012.56 9 | dynamicProperty.intValue=42 10 | dynamicProperty.localDateValue=2009-06-12 11 | dynamicProperty.periodValue=00:12:22 12 | dynamicProperty.booleanValue=true 13 | dynamicProperty.localDateTimeValue=2009-07-05 12:56:02 14 | invalid.period=12:22: 15 | dynamicProperty.bigIntegerValue=224411 16 | dynamicProperty.dateValue=14-4-2017 17 | -------------------------------------------------------------------------------- /src/test/resources/test-files/fileWatcher.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 15 12:18:45 BST 2012 2 | dynamicProperty.longValue=12345 3 | dynamicProperty.substitutionProperty=${dynamicProperty.substitutionValue} 4 | dynamicProperty.doubleValue=12345.67 5 | dynamicProperty.localTimeValue=12:22:45 6 | dynamicProperty.stringValue=Injected String Value 7 | dynamicProperty.substitutionValue=elephant 8 | dynamicProperty.bigDecimalValue=20012.56 9 | dynamicProperty.intValue=42 10 | dynamicProperty.localDateValue=2009-06-12 11 | dynamicProperty.periodValue=00:12:22 12 | dynamicProperty.booleanValue=true 13 | dynamicProperty.localDateTimeValue=2009-07-05 12:56:02 14 | invalid.period=12:22: 15 | dynamicProperty.bigIntegerValue=224411 16 | -------------------------------------------------------------------------------- /src/test/resources/test-files/reloading.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 15 13:22:10 BST 2012 2 | dynamicProperty.stringValue=Injected String Value 3 | dynamicProperty.compoiteStringValue=Hello, ${dynamicProperty.baseStringValue}! 4 | dynamicProperty.baseStringValue=World -------------------------------------------------------------------------------- /src/test/resources/test-files2/fileWatcher2.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 15 12:18:45 BST 2012 2 | dynamicProperty.longValue=12345 3 | dynamicProperty.substitutionProperty=${dynamicProperty.substitutionValue} 4 | dynamicProperty.doubleValue=12345.67 5 | dynamicProperty.localTimeValue=12:22:45 6 | dynamicProperty.stringValue=Injected String Value 7 | dynamicProperty.substitutionValue=elephant 8 | dynamicProperty.bigDecimalValue=20012.56 9 | dynamicProperty.intValue=42 10 | dynamicProperty.localDateValue=2009-06-12 11 | dynamicProperty.periodValue=00:12:22 12 | dynamicProperty.booleanValue=true 13 | dynamicProperty.localDateTimeValue=2009-07-05 12:56:02 14 | invalid.period=12:22: 15 | dynamicProperty.bigIntegerValue=224411 16 | --------------------------------------------------------------------------------