├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── integrationTest ├── java │ └── com │ │ └── spring │ │ └── loader │ │ ├── PojoBindingTest.java │ │ └── configuration │ │ ├── BasicTestConfigPojo.java │ │ ├── SpringBootTestApplication.java │ │ └── TestNestedPropertiesYaml.java └── resources │ ├── basic-props.properties │ └── test-nested-properties.yml ├── main └── java │ └── com │ └── spring │ └── loader │ ├── S3PropertiesLocation.java │ ├── cloud │ ├── S3PropertiesContext.java │ ├── S3PropertySource.java │ ├── S3Service.java │ └── S3StreamLoader.java │ ├── configuration │ ├── S3PropertiesLoaderConfiguration.java │ ├── S3PropertiesLocationRegistrar.java │ └── S3PropertiesSourceConfigurer.java │ ├── exception │ ├── EnviromentPropertyNotFoundException.java │ ├── InvalidS3LocationException.java │ ├── S3ContextRefreshException.java │ └── S3ResourceException.java │ └── util │ ├── SystemPropertyResolver.java │ └── WordUtils.java └── test ├── java └── com │ └── spring │ └── loader │ ├── cloud │ └── S3ServiceTest.java │ ├── configuration │ ├── S3PropertiesLocationRegistrarTest.java │ └── S3PropertiesSourceConfigurerTest.java │ └── util │ └── SystemPropertyResolverTest.java └── resources ├── external-config.properties └── external-config.yaml /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 11 15 | - name: Unit and Integration Tests 16 | run: ./gradlew clean test integrationTest 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .settings 3 | .project 4 | .class 5 | .DS_Store 6 | .bin 7 | .lock 8 | .gradle 9 | target/ 10 | build/ 11 | bin/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Dallo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ericdallo/spring-s3-properties-loader.svg?branch=master)](https://travis-ci.org/ericdallo/spring-s3-properties-loader) 2 | # Spring S3 Property Loader 3 | 4 | 5 | _S3 Property Loader_ has the aim of allowing loading of Spring property files from S3 bucket, in order to guarantee stateless machine configuration. 6 | 7 | Spring PropertyConfigurer uses `PropertiesFactoryBean` to load property files from *AWS S3* bucket. 8 | 9 | ## Install 10 | _Gradle_: 11 | ```groovy 12 | repositories { 13 | jcenter() 14 | } 15 | ``` 16 | ```groovy 17 | compile "com.spring.loader:s3-loader:3.0.0" 18 | ``` 19 | _Maven_: 20 | ```xml 21 | 22 | com.spring.loader 23 | s3-loader 24 | 3.0.0 25 | pom 26 | 27 | ``` 28 | 29 | ## How to use 30 | 31 | - Adding this annotation to any spring managed bean 32 | ```java 33 | @S3PropertiesLocation("my-bucket/my-folder/my-properties.yaml") 34 | ``` 35 | - Using a specific profile to only load properties if the app is running with that profile 36 | ```java 37 | @S3PropertiesLocation(value = "my-bucket/my-folder/my-properties.properties", profiles = "production") 38 | ``` 39 | - Load from a System env variable 40 | ```java 41 | @S3PropertiesLocation(value = "${AWS_S3_LOCATION}", profiles = "developer") 42 | // or 43 | @S3PropertiesLocation(value = "${AWS_S3_BUCKET}/application/my.properties", profiles = "developer") 44 | ``` 45 | 46 | ### Binding properties to a POJO 47 | You can bind the externally loaded properties to a POJO as well. 48 | 49 | For e.g., if you have a YAML file as 50 | ```yaml 51 | zuul: 52 | routes: 53 | query1: 54 | path: /api/apps/test1/query/** 55 | stripPrefix: false 56 | url: "https://test.url.com/query1" 57 | query2: 58 | path: /api/apps/test2/query/** 59 | stripPrefix: false 60 | url: "https://test.url.com/query2" 61 | index1: 62 | path: /api/apps/*/index/** 63 | stripPrefix: false 64 | url: "https://test.url.com/index" 65 | ``` 66 | Then you can bind the properties to a POJO using ConfigurationProperties: 67 | ```java 68 | @Component 69 | @ConfigurationProperties("zuul") 70 | public class RouteConfig { 71 | private Map> routes = new HashMap<>(); 72 | 73 | public void setRoutes(Map> routes) { 74 | this.routes = routes; 75 | } 76 | 77 | public Map> getRoutes() { 78 | return routes; 79 | } 80 | } 81 | 82 | // or 83 | 84 | @Component 85 | @ConfigurationProperties("zuul") 86 | public class RouteConfig { 87 | private Map routes; 88 | 89 | public void setRoutes(Map routes) { 90 | this.routes = routes; 91 | } 92 | 93 | public Map getRoutes() { 94 | return routes; 95 | } 96 | 97 | public static class Route { 98 | private String path; 99 | private boolean stripPrefix; 100 | String url; 101 | 102 | public String getPath() { 103 | return path; 104 | } 105 | 106 | public void setPath(String path) { 107 | this.path = path; 108 | } 109 | 110 | public boolean isStripPrefix() { 111 | return stripPrefix; 112 | } 113 | 114 | public void setStripPrefix(boolean stripPrefix) { 115 | this.stripPrefix = stripPrefix; 116 | } 117 | 118 | public String getUrl() { 119 | return url; 120 | } 121 | 122 | public void setUrl(String url) { 123 | this.url = url; 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | try { 129 | return new ObjectMapper().writeValueAsString(this); 130 | } catch (JsonProcessingException e) { 131 | e.printStackTrace(); 132 | } 133 | return this.toString(); 134 | } 135 | } 136 | 137 | @Override 138 | public String toString() { 139 | try { 140 | return new ObjectMapper().writeValueAsString(this); 141 | } catch (JsonProcessingException e) { 142 | e.printStackTrace(); 143 | } 144 | return this.toString(); 145 | } 146 | } 147 | ``` 148 | 149 | ### Refreshing properties in runtime 150 | 151 | You can force your application to load properties from S3 again without restart. _S3 Properties Loader_ uses a [Spring Cloud](http://projects.spring.io/spring-cloud/) feature that allows the spring beans annotated with `@RefreshScope` to reload properties. 152 | To work, *it is only necessary* to inject the `S3PropertiesContext` bean and call `refresh()` method. After this, _S3 Properties Loader_ will get properties again from s3 bucket defined previously and refresh your beans annotated with `@RefreshScope`. 153 | 154 | _tip_: You can create a endpoint that calls this class and refresh your application via endpoint or create a `@Scheduled` class which updates from time to time. 155 | 156 | Example: 157 | ```java 158 | @RestController 159 | public SomeController { 160 | 161 | @Autowired 162 | private S3PropertiesContext s3PropertiesContext; 163 | 164 |   @PostMapping("/refresh-properties") 165 | public void refresh() { 166 | s3PropertiesContext.refresh(); 167 | } 168 | } 169 | ``` 170 | ## Requisites 171 | 172 | Official [spring aws sdk lib](https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-aws). 173 | 174 | ## Problems and Issues 175 | 176 | Found some bug? Have some enhancement ? Open a Issue [here](https://github.com/ericdallo/spring-s3-properties-loader/issues) 177 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.plugin.SpringBootPlugin 2 | 3 | plugins { 4 | id 'java' 5 | id 'eclipse' 6 | id 'application' 7 | id 'net.researchgate.release' version '2.6.0' 8 | id 'maven-publish' 9 | id 'maven' 10 | id 'signing' 11 | id 'com.jfrog.bintray' version '1.8.5' 12 | 13 | id 'org.springframework.boot' version '2.3.5.RELEASE' apply false 14 | 15 | // for separating out unit and integration tests 16 | id 'org.unbroken-dome.test-sets' version '3.0.1' 17 | } 18 | 19 | compileJava.options.encoding = 'UTF-8' 20 | 21 | group = 'com.spring.loader' 22 | archivesBaseName = 's3-loader' 23 | 24 | eclipse { 25 | classpath { 26 | downloadJavadoc = true 27 | downloadSources = true 28 | } 29 | } 30 | 31 | release { 32 | failOnCommitNeeded = false 33 | failOnPublishNeeded = true 34 | failOnSnapshotDependencies = true 35 | failOnUnversionedFiles = true 36 | failOnUpdateNeeded = true 37 | revertOnFail = true 38 | } 39 | 40 | afterReleaseBuild.dependsOn publish 41 | 42 | bintray { 43 | user = System.getenv('BINTRAY_USER') 44 | key = System.getenv('BINTRAY_KEY') 45 | configurations = ['archives'] 46 | pkg { 47 | repo = 'spring-properties-loader' 48 | name = 'spring-s3-properties-loader' 49 | licenses = ['Apache-2.0'] 50 | vcsUrl = 'https://github.com/ericdallo/spring-s3-properties-loader.git' 51 | } 52 | } 53 | 54 | repositories { 55 | mavenCentral() 56 | mavenLocal() 57 | } 58 | 59 | dependencies { 60 | // annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 61 | 62 | implementation "org.springframework:spring-context:5.3.0" 63 | implementation "org.springframework.cloud:spring-cloud-aws-core:2.2.4.RELEASE" 64 | implementation "org.springframework.cloud:spring-cloud-context:2.2.5.RELEASE" 65 | 66 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' 67 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' 68 | 69 | testImplementation "org.assertj:assertj-core:3.18.0" 70 | testImplementation "org.mockito:mockito-inline:3.6.0" 71 | testImplementation "org.mockito:mockito-junit-jupiter:3.6.0" 72 | testRuntimeOnly "org.yaml:snakeyaml:1.27" 73 | 74 | testImplementation enforcedPlatform(SpringBootPlugin.BOM_COORDINATES) 75 | testImplementation 'cloud.localstack:localstack-utils:0.2.5' 76 | testImplementation 'com.amazonaws:aws-java-sdk-s3:1.11.896' 77 | testImplementation 'org.springframework.boot:spring-boot-starter-web' 78 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 79 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 80 | } 81 | } 82 | 83 | task sourcesJar(type: Jar) { 84 | from sourceSets.main.allSource 85 | archiveClassifier = 'sources' 86 | } 87 | 88 | artifacts { 89 | archives jar 90 | archives sourcesJar 91 | } 92 | 93 | testSets { 94 | integrationTest 95 | } 96 | 97 | check.dependsOn integrationTest 98 | 99 | // Make all tests use JUnit 5 100 | tasks.withType(Test) { 101 | useJUnitPlatform() 102 | } 103 | 104 | mainClassName = '' 105 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=3.0.0 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdallo/spring-s3-properties-loader/0e6e708cfe5cda5869b5675921d8a006253b437b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/spring/loader/PojoBindingTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader; 2 | 3 | import cloud.localstack.awssdkv1.TestUtils; 4 | import cloud.localstack.docker.LocalstackDockerExtension; 5 | import cloud.localstack.docker.annotation.LocalstackDockerProperties; 6 | import com.amazonaws.services.s3.AmazonS3; 7 | import com.spring.loader.configuration.BasicTestConfigPojo; 8 | import com.spring.loader.configuration.SpringBootTestApplication; 9 | import com.spring.loader.configuration.TestNestedPropertiesYaml; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.boot.test.context.TestConfiguration; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Import; 19 | import org.springframework.context.annotation.Primary; 20 | 21 | import java.nio.file.Paths; 22 | 23 | import static org.junit.jupiter.api.Assertions.*; 24 | 25 | @ExtendWith(LocalstackDockerExtension.class) 26 | @LocalstackDockerProperties(services = { "s3" }) 27 | @Import( { S3Config.class, BasicTestConfigPojo.class, TestNestedPropertiesYaml.class }) 28 | @S3PropertiesLocation( { "integration-test/basic-props.properties", "integration-test/test-nested-properties.yml" } ) 29 | @SpringBootTest (classes = { SpringBootTestApplication.class }, webEnvironment = SpringBootTest.WebEnvironment.NONE) 30 | public class PojoBindingTest { 31 | 32 | @Autowired 33 | AmazonS3 amazonS3Client; 34 | 35 | @Autowired 36 | TestNestedPropertiesYaml testNestedPropertiesYaml; 37 | 38 | @Autowired 39 | BasicTestConfigPojo basicTestConfigPojo; 40 | 41 | @Value("${test.property1}") 42 | String testProp1; 43 | 44 | @BeforeAll 45 | public static void init() { 46 | AmazonS3 s3 = TestUtils.getClientS3(); 47 | 48 | s3.createBucket("integration-test"); 49 | s3.putObject("integration-test", "test-nested-properties.yml", 50 | Paths.get("./src/integrationTest/resources/test-nested-properties.yml").toFile()); 51 | s3.putObject("integration-test", "basic-props.properties", 52 | Paths.get("./src/integrationTest/resources/basic-props.properties").toFile()); 53 | } 54 | 55 | @Test 56 | public void testS3PropertiesAreBoundedToPojo() { 57 | assertEquals("value1", testProp1); 58 | assertEquals("value1", basicTestConfigPojo.getProperty1()); 59 | assertEquals("value2", basicTestConfigPojo.getProperty2()); 60 | 61 | assertEquals(testNestedPropertiesYaml.getRoutes().size(), 3); 62 | assertNotNull(testNestedPropertiesYaml.getRoutes().get("index")); 63 | assertNotNull(testNestedPropertiesYaml.getRoutes().get("query1")); 64 | assertNotNull(testNestedPropertiesYaml.getRoutes().get("query2")); 65 | assertTrue(testNestedPropertiesYaml.getRoutes().get("index").isStripPrefix()); 66 | assertEquals("https://test.url.com/query1", testNestedPropertiesYaml.getRoutes().get("query1").getUrl()); 67 | assertEquals("/api/apps/test2/query/**", testNestedPropertiesYaml.getRoutes().get("query2").getPath()); 68 | } 69 | } 70 | 71 | @TestConfiguration 72 | class S3Config { 73 | 74 | @Primary 75 | @Bean 76 | public AmazonS3 amazonS3Client() { 77 | return TestUtils.getClientS3(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/spring/loader/configuration/BasicTestConfigPojo.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.boot.test.context.TestConfiguration; 5 | 6 | @TestConfiguration 7 | @ConfigurationProperties("test") 8 | public class BasicTestConfigPojo { 9 | String property1; 10 | String property2; 11 | 12 | public String getProperty1() { 13 | return property1; 14 | } 15 | 16 | public void setProperty1(String property1) { 17 | this.property1 = property1; 18 | } 19 | 20 | public String getProperty2() { 21 | return property2; 22 | } 23 | 24 | public void setProperty2(String property2) { 25 | this.property2 = property2; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return new StringBuilder().append("{ ") 31 | .append("property1=").append(property1) 32 | .append(", ") 33 | .append("property2=").append(property2) 34 | .append(" }").toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/spring/loader/configuration/SpringBootTestApplication.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | 7 | @SpringBootApplication 8 | @EnableConfigurationProperties 9 | public class SpringBootTestApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(SpringBootTestApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/integrationTest/java/com/spring/loader/configuration/TestNestedPropertiesYaml.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.boot.test.context.TestConfiguration; 5 | 6 | import java.util.Collections; 7 | import java.util.Map; 8 | 9 | @TestConfiguration 10 | @ConfigurationProperties("zuul") 11 | public class TestNestedPropertiesYaml { 12 | private Map routes = Collections.emptyMap(); 13 | 14 | public void setRoutes(Map routes) { 15 | this.routes = routes; 16 | } 17 | 18 | public Map getRoutes() { 19 | return routes; 20 | } 21 | 22 | public static class Route { 23 | private String path; 24 | private boolean stripPrefix; 25 | private String url; 26 | 27 | public String getPath() { 28 | return path; 29 | } 30 | 31 | public void setPath(String path) { 32 | this.path = path; 33 | } 34 | 35 | public boolean isStripPrefix() { 36 | return stripPrefix; 37 | } 38 | 39 | public void setStripPrefix(boolean stripPrefix) { 40 | this.stripPrefix = stripPrefix; 41 | } 42 | 43 | public String getUrl() { 44 | return url; 45 | } 46 | 47 | public void setUrl(String url) { 48 | this.url = url; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return new StringBuilder().append("{ ") 54 | .append("path=").append(path) 55 | .append(", ") 56 | .append("stripPrefix=").append(stripPrefix) 57 | .append(", ") 58 | .append("url=").append(url) 59 | .append(" }").toString(); 60 | } 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "{ routes = " + routes + " }"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/integrationTest/resources/basic-props.properties: -------------------------------------------------------------------------------- 1 | test.property1=value1 2 | test.property2=value2 3 | -------------------------------------------------------------------------------- /src/integrationTest/resources/test-nested-properties.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | query1: 4 | path: /api/apps/test1/query/** 5 | stripPrefix: false 6 | url: "https://test.url.com/query1" 7 | query2: 8 | path: /api/apps/test2/query/** 9 | stripPrefix: false 10 | url: "https://test.url.com/query2" 11 | index: 12 | path: /api/apps/*/index/** 13 | stripPrefix: true 14 | url: "https://test.url.com/index" 15 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/S3PropertiesLocation.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | import org.springframework.context.annotation.Import; 10 | 11 | import com.spring.loader.configuration.S3PropertiesLocationRegistrar; 12 | import com.spring.loader.cloud.S3PropertySource; 13 | import com.spring.loader.configuration.S3PropertiesLoaderConfiguration; 14 | 15 | /** 16 | * Allow the auto configuration of the {@link S3PropertySource} bean. 17 | * 18 | * @author Eric Dallo 19 | * @since 1.0.3 20 | * @see S3PropertiesLocationRegistrar 21 | */ 22 | @Target(ElementType.TYPE) 23 | @Retention(RetentionPolicy.RUNTIME) 24 | @Import({ S3PropertiesLoaderConfiguration.class, S3PropertiesLocationRegistrar.class }) 25 | @Documented 26 | public @interface S3PropertiesLocation { 27 | 28 | /** 29 | * The location of the properties in aws s3. 30 | * 31 | * @return the path of aws s3 properties e.g. "my-bucket/my-folder/app.properties" 32 | * or a enviroment system to the s3 path e.g. "${MY_BUCKET_IN_AWS_S3}" 33 | */ 34 | String[] value(); 35 | 36 | /** 37 | * The profiles to load the properties in aws s3. 38 | * 39 | * @return the profile name e.g. "prod" 40 | */ 41 | String[] profiles() default {}; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/cloud/S3PropertiesContext.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.cloud; 2 | 3 | import static org.springframework.util.ClassUtils.getUserClass; 4 | 5 | import java.util.Map.Entry; 6 | import java.util.Properties; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.cloud.context.config.annotation.RefreshScope; 11 | import org.springframework.cloud.context.environment.EnvironmentManager; 12 | import org.springframework.cloud.context.refresh.ContextRefresher; 13 | import org.springframework.context.ApplicationContext; 14 | 15 | import com.amazonaws.services.s3.model.S3Object; 16 | import com.spring.loader.S3PropertiesLocation; 17 | import com.spring.loader.exception.S3ContextRefreshException; 18 | 19 | /** 20 | * Manage the context of properties from S3 21 | * 22 | * @author Eric Dallo 23 | * @since 2.1 24 | * @see S3PropertiesLocation 25 | */ 26 | public class S3PropertiesContext { 27 | 28 | private static final Logger LOGGER = LoggerFactory.getLogger(S3PropertiesContext.class); 29 | 30 | private final ApplicationContext applicationContext; 31 | private final EnvironmentManager environment; 32 | private final ContextRefresher contextRefresher; 33 | private final S3Service s3Service; 34 | 35 | public S3PropertiesContext(ApplicationContext applicationContext, EnvironmentManager environment, ContextRefresher contextRefresher, S3Service s3Service) { 36 | this.applicationContext = applicationContext; 37 | this.environment = environment; 38 | this.contextRefresher = contextRefresher; 39 | this.s3Service = s3Service; 40 | } 41 | 42 | /** 43 | * Allows the feature of refresh beans annotated with {@link RefreshScope} of spring cloud. 44 | * The annotated beans will be updated with the new properties of the location setted previously 45 | * from {@link S3PropertiesLocation#value() } 46 | * 47 | * @throws S3ContextRefreshException for any error on refresh properties 48 | * @see RefreshScope 49 | */ 50 | public void refresh() { 51 | try { 52 | Object annotatedBean = applicationContext.getBeansWithAnnotation(S3PropertiesLocation.class).values().iterator().next(); 53 | 54 | String[] locations = getUserClass(annotatedBean).getAnnotation(S3PropertiesLocation.class).value(); 55 | 56 | for (String location : locations) { 57 | 58 | Properties properties = new Properties(); 59 | S3Object s3Object = s3Service.retriveFrom(location); 60 | 61 | properties.load(s3Object.getObjectContent()); 62 | 63 | for (Entry entry : properties.entrySet()) { 64 | LOGGER.debug("Loading property '{}={}'", entry.getKey(), entry.getValue()); 65 | environment.setProperty(entry.getKey().toString(), entry.getValue().toString()); 66 | } 67 | } 68 | 69 | LOGGER.info("Refreshing properties retrieved from S3"); 70 | contextRefresher.refresh(); 71 | } catch (Exception e) { 72 | throw new S3ContextRefreshException("Could not refresh properties from S3", e); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/cloud/S3PropertySource.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.cloud; 2 | 3 | import java.util.Properties; 4 | 5 | import org.springframework.core.env.EnumerablePropertySource; 6 | import org.springframework.core.env.PropertySource; 7 | import org.springframework.lang.Nullable; 8 | 9 | /** 10 | * A PropertySource implementation that reads the properties from an S3 bucket. 11 | * The config file in S3 can be a properties file or YAML file. 12 | * 13 | * Most framework-provided {@link PropertySource} implementations are enumerable. Spring 14 | * does binding of source properties to a Map only for enumerable sources. 15 | * @see org.springframework.boot.context.properties.bind.MapBinder 16 | * When possible the SpringIterableConfigurationPropertySource will be used in preference 17 | * to SpringConfigurationPropertySource since it supports full "relaxed" style resolution. 18 | */ 19 | public class S3PropertySource extends EnumerablePropertySource { 20 | 21 | private static final String S3_PROPERTY_SOURCE_NAME = "s3PropertySource"; 22 | 23 | private final Properties properties; 24 | 25 | public S3PropertySource (Properties properties) { 26 | super(S3_PROPERTY_SOURCE_NAME); 27 | this.properties = properties; 28 | } 29 | 30 | @Override 31 | @Nullable 32 | public Object getProperty(String name) { 33 | return this.properties.get(name); 34 | } 35 | 36 | @Override 37 | public boolean containsProperty(String name) { 38 | return this.properties.containsKey(name); 39 | } 40 | 41 | @Override 42 | public String[] getPropertyNames() { 43 | return this.properties.keySet().toArray(new String[this.properties.size()]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/cloud/S3Service.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.cloud; 2 | 3 | import com.amazonaws.services.s3.AmazonS3; 4 | import com.amazonaws.services.s3.model.S3Object; 5 | import com.spring.loader.exception.InvalidS3LocationException; 6 | import com.spring.loader.exception.S3ResourceException; 7 | import org.springframework.util.ObjectUtils; 8 | 9 | /** 10 | * Bridge to {@link AmazonS3} methods 11 | * 12 | * @author Eric Dallo 13 | * @since 2.1 14 | */ 15 | public class S3Service { 16 | 17 | private static final String S3_PROTOCOL_PREFIX = "s3://"; 18 | private final AmazonS3 amazonS3; 19 | 20 | public S3Service(AmazonS3 amazonS3) { 21 | this.amazonS3 = amazonS3; 22 | } 23 | 24 | /** 25 | * @param location bucketName + key location 26 | * @return {@link S3Object} for the given aws s3 location. 27 | * @throws InvalidS3LocationException for invalid location params 28 | * @throws S3ResourceException for connection and availability errors 29 | */ 30 | public S3Object retriveFrom(String location) { 31 | if (ObjectUtils.isEmpty(location)) { 32 | throw new InvalidS3LocationException("Location cannot be empty or null"); 33 | } 34 | 35 | String path = location.startsWith(S3_PROTOCOL_PREFIX) ? location.substring(S3_PROTOCOL_PREFIX.length(), location.length()) : location; 36 | 37 | if(!path.contains("/")) { 38 | throw new InvalidS3LocationException("The location must contains the full path of the properties file"); 39 | } 40 | 41 | String bucketName = path.substring(0, path.indexOf('/')); 42 | String keyName = path.substring(path.indexOf('/') + 1); 43 | 44 | try { 45 | return amazonS3.getObject(bucketName, keyName); 46 | } catch (Exception e) { 47 | throw new S3ResourceException("Could not load resource from " + location, e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/cloud/S3StreamLoader.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.cloud; 2 | 3 | import java.io.InputStream; 4 | 5 | import com.spring.loader.S3PropertiesLocation; 6 | 7 | /** 8 | * Get {@link InputStream} for the given aws s3 location. 9 | * For use with the {@link S3PropertiesLocation} annotation. 10 | * 11 | * @author Eric Dallo 12 | * @since 1.0.0 13 | * @see S3PropertySource 14 | */ 15 | public class S3StreamLoader { 16 | 17 | private final S3Service s3Service; 18 | 19 | public S3StreamLoader(S3Service s3Service) { 20 | this.s3Service = s3Service; 21 | } 22 | 23 | public InputStream getProperty(String location) { 24 | return s3Service.retriveFrom(location).getObjectContent(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/configuration/S3PropertiesLoaderConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import org.springframework.cloud.context.environment.EnvironmentManager; 4 | import org.springframework.cloud.context.refresh.ContextRefresher; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import com.amazonaws.services.s3.AmazonS3; 10 | import com.spring.loader.cloud.S3PropertiesContext; 11 | import com.spring.loader.cloud.S3StreamLoader; 12 | import com.spring.loader.cloud.S3Service; 13 | 14 | @Configuration 15 | public class S3PropertiesLoaderConfiguration { 16 | 17 | @Bean 18 | S3Service s3Service(AmazonS3 amazonS3) { 19 | return new S3Service(amazonS3); 20 | } 21 | 22 | @Bean 23 | S3StreamLoader s3ResourceLoader(S3Service s3Service) { 24 | return new S3StreamLoader(s3Service); 25 | } 26 | 27 | @Bean 28 | S3PropertiesContext refreshProperties(ApplicationContext applicationContext, EnvironmentManager environmentManager, ContextRefresher contextRefresher, S3Service s3Service) { 29 | return new S3PropertiesContext(applicationContext, environmentManager, contextRefresher, s3Service); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/configuration/S3PropertiesLocationRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import static com.spring.loader.util.WordUtils.classNameloweredCaseFirstLetter; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.config.BeanDefinition; 8 | import org.springframework.beans.factory.config.RuntimeBeanReference; 9 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 10 | import org.springframework.beans.factory.support.RootBeanDefinition; 11 | import org.springframework.context.EnvironmentAware; 12 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 13 | import org.springframework.core.annotation.AnnotationAttributes; 14 | import org.springframework.core.env.Environment; 15 | import org.springframework.core.env.Profiles; 16 | import org.springframework.core.type.AnnotationMetadata; 17 | 18 | import com.spring.loader.S3PropertiesLocation; 19 | import com.spring.loader.cloud.S3PropertySource; 20 | import com.spring.loader.util.SystemPropertyResolver; 21 | 22 | /** 23 | * Creates the {@link S3PropertySource} bean. 24 | * For use with the {@link S3PropertiesLocation} annotation. 25 | * 26 | * @author Eric Dallo 27 | * @since 1.0.3 28 | * @see S3PropertiesLocation 29 | * @see S3PropertySource 30 | */ 31 | public class S3PropertiesLocationRegistrar implements EnvironmentAware, ImportBeanDefinitionRegistrar { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(S3PropertiesLocationRegistrar.class); 34 | 35 | private Environment environment; 36 | private SystemPropertyResolver resolver; 37 | 38 | public S3PropertiesLocationRegistrar() { 39 | resolver = new SystemPropertyResolver(); 40 | } 41 | 42 | public S3PropertiesLocationRegistrar(Environment environment, SystemPropertyResolver resolver) { 43 | this.environment = environment; 44 | this.resolver = resolver; 45 | } 46 | 47 | @Override 48 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 49 | AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(S3PropertiesLocation.class.getName())); 50 | String[] profiles = attributes.getStringArray("profiles"); 51 | 52 | if (profiles.length > 0 && !environment.acceptsProfiles(Profiles.of(profiles))) { 53 | LOGGER.warn("S3 Properties not loaded. Current application profile: {}. Acceptable profiles: {}", environment.getActiveProfiles(), profiles); 54 | return; 55 | } 56 | 57 | String[] locations = attributes.getStringArray("value"); 58 | 59 | String[] formattedLocations = new String[locations.length]; 60 | 61 | for (int i = 0; i < locations.length; i++) { 62 | formattedLocations[i] = resolver.getFormattedValue(locations[i]); 63 | } 64 | 65 | BeanDefinition configurerDefinition = new RootBeanDefinition(S3PropertiesSourceConfigurer.class); 66 | configurerDefinition.getPropertyValues().addPropertyValue("s3ResourceLoader", new RuntimeBeanReference("s3ResourceLoader")); 67 | configurerDefinition.getPropertyValues().add("locations", formattedLocations); 68 | 69 | registry.registerBeanDefinition(classNameloweredCaseFirstLetter(S3PropertiesSourceConfigurer.class), configurerDefinition); 70 | } 71 | 72 | @Override 73 | public void setEnvironment(Environment environment) { 74 | this.environment = environment; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/configuration/S3PropertiesSourceConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import java.io.IOException; 4 | import java.util.Map.Entry; 5 | import java.util.Optional; 6 | import java.util.Properties; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.BeansException; 11 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 12 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 13 | import org.springframework.beans.factory.config.PropertiesFactoryBean; 14 | import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; 15 | import org.springframework.context.EnvironmentAware; 16 | import org.springframework.core.Ordered; 17 | import org.springframework.core.PriorityOrdered; 18 | import org.springframework.core.env.ConfigurableEnvironment; 19 | import org.springframework.core.env.Environment; 20 | import org.springframework.core.env.MutablePropertySources; 21 | import org.springframework.core.env.PropertySource; 22 | 23 | import com.spring.loader.S3PropertiesLocation; 24 | import com.spring.loader.cloud.S3PropertySource; 25 | import com.spring.loader.cloud.S3StreamLoader; 26 | import org.springframework.core.io.InputStreamResource; 27 | 28 | /** 29 | * Add a new {@link PropertySource} to spring property sources from a S3 bucket 30 | * For use with the {@link S3PropertiesLocation} annotation. 31 | * 32 | * @author Eric Dallo 33 | * @since 2.0 34 | * @see S3PropertiesLocation 35 | * @see S3PropertySource 36 | */ 37 | public class S3PropertiesSourceConfigurer implements EnvironmentAware, BeanFactoryPostProcessor, PriorityOrdered { 38 | 39 | private static final Logger LOGGER = LoggerFactory.getLogger(S3PropertiesSourceConfigurer.class); 40 | 41 | private Environment environment; 42 | private S3StreamLoader s3ResourceLoader; 43 | private String[] locations; 44 | 45 | public void setS3ResourceLoader(S3StreamLoader s3ResourceLoader) { 46 | this.s3ResourceLoader = s3ResourceLoader; 47 | } 48 | 49 | public void setLocations(String[] locations) { 50 | this.locations = locations; 51 | } 52 | 53 | @Override 54 | public void setEnvironment(Environment environment) { 55 | this.environment = environment; 56 | } 57 | 58 | @Override 59 | public int getOrder() { 60 | return Ordered.HIGHEST_PRECEDENCE; 61 | } 62 | 63 | @Override 64 | public void postProcessBeanFactory(@SuppressWarnings("unused") ConfigurableListableBeanFactory beanFactory) throws BeansException { 65 | if (this.environment instanceof ConfigurableEnvironment) { 66 | 67 | PropertiesFactoryBean propertiesFactory = new PropertiesFactoryBean(); 68 | MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); 69 | 70 | propertiesFactory.setSingleton(false); 71 | 72 | Properties[] propertiesToAdd = new Properties[locations.length]; 73 | 74 | LOGGER.info("Starting to load properties from S3 into application"); 75 | 76 | for (int i = 0; i < locations.length; i++) { 77 | Properties properties = loadProperties(locations[i]); 78 | propertiesToAdd[i] = properties; 79 | 80 | for (Entry entry : properties.entrySet()) { 81 | LOGGER.debug("Loading property '{}={}'", entry.getKey(), entry.getValue()); 82 | } 83 | } 84 | 85 | propertiesFactory.setPropertiesArray(propertiesToAdd); 86 | 87 | try { 88 | propertiesFactory.afterPropertiesSet(); 89 | propertySources.addFirst(new S3PropertySource(propertiesFactory.getObject())); 90 | 91 | LOGGER.info("Successfully loaded properties from S3 into application"); 92 | } catch (IOException e) { 93 | LOGGER.error("Could not read properties from s3Location", e); 94 | } 95 | 96 | } else { 97 | LOGGER.warn("Environment is not of type '{}' property source with instance data is not available", ConfigurableEnvironment.class.getName()); 98 | } 99 | } 100 | 101 | Properties loadProperties(String s3File) { 102 | String extn = getFileExtension(s3File); 103 | switch(extn) { 104 | case "yaml": 105 | case "yml": 106 | var yamlBean = new YamlPropertiesFactoryBean(); 107 | yamlBean.setSingleton(false); 108 | yamlBean.afterPropertiesSet(); 109 | yamlBean.setResources(new InputStreamResource(s3ResourceLoader.getProperty(s3File))); 110 | return yamlBean.getObject(); 111 | case "properties": 112 | default: 113 | Properties properties = new Properties(); 114 | try { 115 | properties.load(s3ResourceLoader.getProperty(s3File)); 116 | } catch (IOException e) { 117 | LOGGER.error("Could not load properties from location " + s3File, e); 118 | } catch (Exception e) { 119 | LOGGER.error("Error on loading properties from location: " + s3File, e); 120 | } 121 | return properties; 122 | } 123 | } 124 | 125 | private String getFileExtension(String filename) { 126 | return Optional.of(filename.toLowerCase()) 127 | .filter(f -> f.contains(".")) 128 | .map(f -> f.substring(filename.lastIndexOf(".") + 1)) 129 | .get(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/exception/EnviromentPropertyNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.exception; 2 | 3 | public class EnviromentPropertyNotFoundException extends S3ResourceException { 4 | private static final long serialVersionUID = -3434756303367606716L; 5 | 6 | public EnviromentPropertyNotFoundException(String message) { 7 | super(message); 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/exception/InvalidS3LocationException.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.exception; 2 | 3 | public class InvalidS3LocationException extends S3ResourceException { 4 | private static final long serialVersionUID = 2057112790586228669L; 5 | 6 | public InvalidS3LocationException(String message) { 7 | super(message); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/exception/S3ContextRefreshException.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.exception; 2 | 3 | public class S3ContextRefreshException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 7836301699281948051L; 6 | 7 | public S3ContextRefreshException(String message, Exception e) { 8 | super(message, e); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/exception/S3ResourceException.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.exception; 2 | 3 | public class S3ResourceException extends RuntimeException { 4 | private static final long serialVersionUID = 8310280589629514933L; 5 | 6 | public S3ResourceException(String msg) { 7 | super(msg); 8 | } 9 | 10 | public S3ResourceException(String msg, Throwable cause) { 11 | super(msg, cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/util/SystemPropertyResolver.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.util; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import com.spring.loader.exception.EnviromentPropertyNotFoundException; 9 | import com.spring.loader.exception.InvalidS3LocationException; 10 | import org.springframework.util.ObjectUtils; 11 | 12 | /** 13 | * Resolver for properties that will be retrieved from system environment. 14 | * 15 | * @author Eric Dallo 16 | * @since 2.0 17 | */ 18 | public class SystemPropertyResolver { 19 | 20 | private static final String SYSTEM_NOTATION_PREFIX = "${"; 21 | 22 | public String getFormattedValue(String value) { 23 | if (ObjectUtils.isEmpty(value)) { 24 | throw new InvalidS3LocationException("The location cannot be empty or null"); 25 | } 26 | 27 | if (value.contains(SYSTEM_NOTATION_PREFIX)) { 28 | String bucket = value; 29 | String pattern = "\\$\\{([A-Za-z0-9_]+)\\}"; 30 | Matcher matcher = Pattern.compile(pattern).matcher(bucket); 31 | while (matcher.find()) { 32 | String envValue = getFromPropOrEnv(matcher.group(1)); 33 | Pattern subExpression = Pattern.compile(Pattern.quote(matcher.group(0))); 34 | bucket = subExpression.matcher(bucket).replaceAll(envValue); 35 | } 36 | 37 | if (bucket.contains(SYSTEM_NOTATION_PREFIX)) { 38 | throw new InvalidS3LocationException("Syntax error for system property: " + value); 39 | } 40 | 41 | return bucket; 42 | } 43 | 44 | return value; 45 | } 46 | 47 | String getFromPropOrEnv(String key) { 48 | String valueFromEnv = System.getProperty(key); 49 | 50 | if (ObjectUtils.isEmpty(valueFromEnv)) { 51 | valueFromEnv = getFromEnv(key); 52 | 53 | if (ObjectUtils.isEmpty(valueFromEnv)) { 54 | throw new EnviromentPropertyNotFoundException(format("Environment variable %s not found in system and java properties", key)); 55 | } 56 | } 57 | 58 | return valueFromEnv; 59 | } 60 | 61 | String getFromEnv(String key) { return key == null ? null : System.getenv(key); } 62 | } -------------------------------------------------------------------------------- /src/main/java/com/spring/loader/util/WordUtils.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.util; 2 | 3 | import static java.lang.Character.toLowerCase; 4 | 5 | public class WordUtils { 6 | 7 | public static String classNameloweredCaseFirstLetter(Class clazz) { 8 | String clazzName = clazz.getSimpleName(); 9 | return toLowerCase(clazzName.charAt(0)) + clazzName.substring(1); 10 | } 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/spring/loader/cloud/S3ServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.cloud; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertThrows; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.never; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Mock; 13 | 14 | import com.amazonaws.services.s3.AmazonS3; 15 | import com.amazonaws.services.s3.model.S3Object; 16 | import com.amazonaws.services.s3.model.S3ObjectInputStream; 17 | import com.spring.loader.exception.InvalidS3LocationException; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | import org.mockito.junit.jupiter.MockitoSettings; 20 | import org.mockito.quality.Strictness; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | @MockitoSettings(strictness = Strictness.LENIENT) 24 | public class S3ServiceTest { 25 | 26 | private S3Service subject; 27 | 28 | @Mock 29 | private AmazonS3 s3; 30 | @Mock 31 | private S3Object s3Object; 32 | @Mock 33 | private S3ObjectInputStream inputStream; 34 | 35 | private String validLocationWithPrefix = "s3://mybucket/myfolder"; 36 | private String validLocationWithoutPrefix = "mybucket/myfolder"; 37 | private String invalidLocation = "mybucket"; 38 | private String emptyLocation = ""; 39 | 40 | @BeforeEach 41 | public void setup() { 42 | subject = new S3Service(s3); 43 | } 44 | 45 | @Test 46 | public void shouldGetAValidResourceWithPrefix() { 47 | when(s3.getObject(anyString(), anyString())).thenReturn(s3Object); 48 | when(s3Object.getObjectContent()).thenReturn(inputStream); 49 | 50 | subject.retriveFrom(validLocationWithPrefix); 51 | 52 | verify(s3).getObject(anyString(), anyString()); 53 | } 54 | 55 | @Test 56 | public void shouldGetAValidResourceWithoutPrefix() { 57 | when(s3.getObject(anyString(), anyString())).thenReturn(s3Object); 58 | when(s3Object.getObjectContent()).thenReturn(inputStream); 59 | 60 | subject.retriveFrom(validLocationWithoutPrefix); 61 | 62 | verify(s3).getObject(anyString(), anyString()); 63 | } 64 | 65 | @Test 66 | public void shouldNotGetAValidResourceWhenLocationIsEmpty() { 67 | when(s3.getObject(anyString(), anyString())).thenReturn(s3Object); 68 | when(s3Object.getObjectContent()).thenReturn(inputStream); 69 | 70 | assertThrows(InvalidS3LocationException.class, () -> subject.retriveFrom(emptyLocation)); 71 | 72 | verify(s3, never()).getObject(anyString(), anyString()); 73 | } 74 | 75 | @Test 76 | public void shouldNotGetAValidResourceWhenLocationIsInvald() { 77 | when(s3.getObject(anyString(), anyString())).thenReturn(s3Object); 78 | when(s3Object.getObjectContent()).thenReturn(inputStream); 79 | 80 | assertThrows(InvalidS3LocationException.class, () -> subject.retriveFrom(invalidLocation)); 81 | 82 | verify(s3, never()).getObject(anyString(), anyString()); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/spring/loader/configuration/S3PropertiesLocationRegistrarTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.never; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | import org.springframework.beans.factory.config.BeanDefinition; 18 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 19 | import org.springframework.core.env.Environment; 20 | import org.springframework.core.env.Profiles; 21 | import org.springframework.core.type.AnnotationMetadata; 22 | 23 | import com.spring.loader.S3PropertiesLocation; 24 | 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | public class S3PropertiesLocationRegistrarTest { 28 | 29 | private S3PropertiesLocationRegistrar subject; 30 | 31 | private Map attributes; 32 | 33 | @Mock 34 | private Environment environment; 35 | @Mock 36 | private AnnotationMetadata importingClassMetadata; 37 | @Mock 38 | private BeanDefinitionRegistry registry; 39 | 40 | @BeforeEach 41 | public void setup() { 42 | subject = new S3PropertiesLocationRegistrar(); 43 | subject.setEnvironment(environment); 44 | 45 | attributes = new HashMap<>(); 46 | attributes.put("path", new String[] { "my-bucket/my.yaml" }); 47 | attributes.put("value", new String[] { "my-bucket/my.properties" }); 48 | 49 | when(importingClassMetadata.getAnnotationAttributes(S3PropertiesLocation.class.getName())).thenReturn(attributes); 50 | } 51 | 52 | @Test 53 | public void shouldLoadPropertiesWhenThereisNoProfiles() { 54 | attributes.put("profiles", new String[] {}); 55 | 56 | subject.registerBeanDefinitions(importingClassMetadata, registry); 57 | 58 | verify(registry).registerBeanDefinition(anyString(), any(BeanDefinition.class)); 59 | } 60 | 61 | @Test 62 | public void shouldLoadPropertiesWhenThereisProfilesActive() { 63 | String[] profiles = new String[] { "prod" }; 64 | 65 | attributes.put("profiles", profiles); 66 | 67 | when(environment.acceptsProfiles(Profiles.of(profiles))).thenReturn(true); 68 | 69 | subject.registerBeanDefinitions(importingClassMetadata, registry); 70 | 71 | verify(registry).registerBeanDefinition(anyString(), any(BeanDefinition.class)); 72 | } 73 | 74 | @Test 75 | public void shouldNotLoadPropertiesWhenThereisNoProfilesActive() { 76 | String[] profiles = new String[] { "prod" }; 77 | 78 | attributes.put("profiles", profiles); 79 | 80 | when(environment.acceptsProfiles(Profiles.of(profiles))).thenReturn(false); 81 | 82 | subject.registerBeanDefinitions(importingClassMetadata, registry); 83 | 84 | verify(registry, never()).registerBeanDefinition(anyString(), any(BeanDefinition.class)); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/spring/loader/configuration/S3PropertiesSourceConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.configuration; 2 | 3 | import com.spring.loader.cloud.S3StreamLoader; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.Properties; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 14 | import static org.mockito.Mockito.when; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class S3PropertiesSourceConfigurerTest { 18 | 19 | @Mock 20 | private S3StreamLoader s3StreamLoader; 21 | 22 | private S3PropertiesSourceConfigurer s3PropertiesSourceConfigurer; 23 | 24 | @BeforeEach 25 | public void setup() { 26 | 27 | when(s3StreamLoader.getProperty("external-config.properties")) 28 | .thenReturn(getClass().getClassLoader().getResourceAsStream("external-config.properties")); 29 | when(s3StreamLoader.getProperty("external-config.yaml")) 30 | .thenReturn(getClass().getClassLoader().getResourceAsStream("external-config.yaml")); 31 | 32 | s3PropertiesSourceConfigurer = new S3PropertiesSourceConfigurer(); 33 | s3PropertiesSourceConfigurer.setS3ResourceLoader(s3StreamLoader); 34 | } 35 | 36 | @Test 37 | public void shouldParsePropertiesFileAndYamlFileIntoSameProperties() { 38 | Properties properties = s3PropertiesSourceConfigurer.loadProperties("external-config.properties"); 39 | Properties yamlProperties = s3PropertiesSourceConfigurer.loadProperties("external-config.yaml"); 40 | 41 | System.out.println(properties); 42 | System.out.println(yamlProperties); 43 | 44 | assertEquals(properties.size(), yamlProperties.size()); 45 | 46 | yamlProperties.keySet().forEach((key) -> 47 | assertEquals(yamlProperties.getProperty(key.toString()), properties.getProperty(key.toString()))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/spring/loader/util/SystemPropertyResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.loader.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.mockito.ArgumentMatchers.eq; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.spring.loader.exception.EnviromentPropertyNotFoundException; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Spy; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import com.spring.loader.exception.InvalidS3LocationException; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class SystemPropertyResolverTest { 18 | 19 | @Spy 20 | SystemPropertyResolver subject = new SystemPropertyResolver(); 21 | 22 | @Test 23 | public void shouldGetFormattedValueWhenValueIsValid() { 24 | String expected = "someValue"; 25 | 26 | when(subject.getFromEnv("AWS_S3")).thenReturn(expected); 27 | 28 | String env = "${AWS_S3}"; 29 | 30 | String formattedValue = subject.getFormattedValue(env); 31 | 32 | assertEquals(expected, formattedValue); 33 | } 34 | 35 | @Test 36 | public void shouldGetFormattedValueFromPropertiesWhenValueIsValid() { 37 | String expected = "someValue"; 38 | 39 | when(subject.getFromEnv("AWS_S3")).thenReturn(expected); 40 | 41 | String env = "${AWS_S3}"; 42 | 43 | String formattedValue = subject.getFormattedValue(env); 44 | 45 | assertEquals(expected, formattedValue); 46 | } 47 | 48 | @Test 49 | public void shouldGetCombinedFormattedValueWhenValueIsValid() { 50 | String region = "someRegion"; 51 | String environment = "someEnvironment"; 52 | 53 | when(subject.getFromEnv("S3_BUCKET_REGION")).thenReturn(region); 54 | when(subject.getFromEnv("S3_BUCKET_ENVIRONMENT")).thenReturn(environment); 55 | 56 | String configValue = "${S3_BUCKET_REGION}/${S3_BUCKET_ENVIRONMENT}/myApplication/application.properties"; 57 | String expected = String.format("%s/%s/myApplication/application.properties", region, environment); 58 | 59 | String formattedValue = subject.getFormattedValue(configValue); 60 | 61 | assertEquals(expected, formattedValue); 62 | } 63 | 64 | @Test 65 | public void shouldReplaceMultiple() { 66 | String environment = "dev"; 67 | 68 | when(subject.getFromEnv("EC2_ENVIRONMENT")).thenReturn(environment); 69 | 70 | String configValue = "region-${EC2_ENVIRONMENT}/deploy-${EC2_ENVIRONMENT}/application.properties"; 71 | String expected = String.format("region-%s/deploy-%s/application.properties", environment, environment); 72 | 73 | String formattedValue = subject.getFormattedValue(configValue); 74 | 75 | assertEquals(expected, formattedValue); 76 | } 77 | 78 | @Test 79 | public void shouldGetFormattedValueWhenValueIsValidAndNotASystemEnv() { 80 | String expected = "someValue"; 81 | 82 | String formattedValue = subject.getFormattedValue(expected); 83 | 84 | assertEquals(expected, formattedValue); 85 | } 86 | 87 | @Test 88 | public void shouldNotGetFormattedValueWhenValueIsEmpty() { 89 | String expected = ""; 90 | 91 | assertThrows(InvalidS3LocationException.class, () -> subject.getFormattedValue(expected)); 92 | } 93 | 94 | @Test 95 | public void shouldNotGetFormattedValueWhenValueHasAInvalidSyntax() { 96 | String expected = "${AWS_S3"; 97 | 98 | assertThrows(InvalidS3LocationException.class, () -> subject.getFormattedValue(expected)); 99 | } 100 | 101 | @Test 102 | public void shouldNotGetFormattedValueWhenSystemDoesNotHasTheEnvironmentVariable() { 103 | when(subject.getFromEnv(eq("AWS_S3"))).thenReturn(null); 104 | 105 | String env = "${AWS_S3}"; 106 | 107 | assertThrows(EnviromentPropertyNotFoundException.class, () -> subject.getFormattedValue(env)); 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/test/resources/external-config.properties: -------------------------------------------------------------------------------- 1 | zuul.routes.query1.path=/api/apps/test1/query/** 2 | zuul.routes.query1.stripPrefix=false 3 | zuul.routes.query1.url=https://test.url.com/query1 4 | zuul.routes.query2.path=/api/apps/test2/query/** 5 | zuul.routes.query2.stripPrefix=false 6 | zuul.routes.query2.url=https://test.url.com/query2 7 | zuul.routes.index1.path=/api/apps/*/index/** 8 | zuul.routes.index1.stripPrefix=false 9 | zuul.routes.index1.url=https://test.url.com/index 10 | -------------------------------------------------------------------------------- /src/test/resources/external-config.yaml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | query1: 4 | path: /api/apps/test1/query/** 5 | stripPrefix: false 6 | url: "https://test.url.com/query1" 7 | query2: 8 | path: /api/apps/test2/query/** 9 | stripPrefix: false 10 | url: "https://test.url.com/query2" 11 | index1: 12 | path: /api/apps/*/index/** 13 | stripPrefix: false 14 | url: "https://test.url.com/index" 15 | --------------------------------------------------------------------------------