├── Jenkinsfile ├── README.md ├── pom.xml ├── sampleapp.iml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── test │ │ │ └── sampleapp │ │ │ ├── Application.java │ │ │ ├── SwaggerConfiguration.java │ │ │ ├── async │ │ │ └── package-info.java │ │ │ ├── config │ │ │ ├── ApplicationConfig.java │ │ │ └── package-info.java │ │ │ ├── domain │ │ │ ├── ApplicationItem.java │ │ │ └── package-info.java │ │ │ ├── exceptions │ │ │ ├── NotAllowedOperationException.java │ │ │ ├── ResourceNotFoundException.java │ │ │ └── package-info.java │ │ │ ├── repository │ │ │ ├── ApplicationRepository.java │ │ │ └── package-info.java │ │ │ ├── service │ │ │ ├── ApplicationService.java │ │ │ ├── ExternalServiceWithSpringRetryAndCircuitBreaker.java │ │ │ ├── ScheduledServiceAction.java │ │ │ └── package-info.java │ │ │ └── web │ │ │ ├── ApplicationController.java │ │ │ ├── ErrorHandler.java │ │ │ ├── dto │ │ │ ├── ApplicationEntry.java │ │ │ ├── CustomErrorResponse.java │ │ │ └── package-info.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.yml │ │ ├── bootstrap.yml │ │ └── logback.xml └── test │ ├── java │ └── com │ │ └── test │ │ └── sampleapp │ │ ├── acceptance │ │ └── ApplicationE2E.java │ │ ├── integration │ │ ├── CucumberIntegrationIT.java │ │ ├── CucumberRoot.java │ │ ├── GetHealthStep.java │ │ └── GetVersionStep.java │ │ ├── retry │ │ ├── Retry.java │ │ ├── RetryException.java │ │ └── RetryRule.java │ │ └── sanity │ │ └── ApplicationSanityCheck_ITT.java │ └── resources │ ├── features │ ├── health.feature │ └── version.feature │ └── logback-test.xml └── target ├── classes ├── application.yml ├── bootstrap.yml ├── com │ └── test │ │ └── sampleapp │ │ ├── Application.class │ │ ├── SwaggerConfiguration.class │ │ ├── config │ │ └── ApplicationConfig.class │ │ ├── domain │ │ ├── ApplicationItem$ApplicationItemBuilder.class │ │ └── ApplicationItem.class │ │ ├── exceptions │ │ ├── NotAllowedOperationException.class │ │ └── ResourceNotFoundException.class │ │ ├── repository │ │ └── ApplicationRepository.class │ │ ├── service │ │ ├── ApplicationService.class │ │ ├── ExternalServiceWithSpringRetryAndCircuitBreaker.class │ │ └── ScheduledServiceAction.class │ │ └── web │ │ ├── ApplicationController.class │ │ ├── ErrorHandler$ERROR_CODE.class │ │ ├── ErrorHandler.class │ │ └── dto │ │ ├── ApplicationEntry$ApplicationEntryBuilder.class │ │ ├── ApplicationEntry.class │ │ └── CustomErrorResponse.class └── logback.xml └── test-classes ├── com └── test │ └── sampleapp │ ├── acceptance │ └── ApplicationE2E.class │ ├── integration │ ├── CucumberIntegrationIT.class │ ├── CucumberRoot.class │ ├── GetHealthStep.class │ └── GetVersionStep.class │ ├── retry │ ├── Retry.class │ ├── RetryException.class │ ├── RetryRule$1.class │ └── RetryRule.class │ └── sanity │ └── ApplicationSanityCheck_ITT.class ├── features ├── health.feature └── version.feature └── logback-test.xml /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | // run on jenkins nodes tha has java 8 label 3 | agent { label 'java8' } 4 | // global env variables 5 | environment { 6 | EMAIL_RECIPIENTS = 'mahmoud.romeh@test.com' 7 | } 8 | stages { 9 | 10 | stage('Build with unit testing') { 11 | steps { 12 | // Run the maven build 13 | script { 14 | // Get the Maven tool. 15 | // ** NOTE: This 'M3' Maven tool must be configured 16 | // ** in the global configuration. 17 | echo 'Pulling...' + env.BRANCH_NAME 18 | def mvnHome = tool 'Maven 3.5.2' 19 | if (isUnix()) { 20 | def targetVersion = getDevVersion() 21 | print 'target build version...' 22 | print targetVersion 23 | sh "'${mvnHome}/bin/mvn' -Dintegration-tests.skip=true -Dbuild.number=${targetVersion} clean package" 24 | def pom = readMavenPom file: 'pom.xml' 25 | // get the current development version 26 | developmentArtifactVersion = "${pom.version}-${targetVersion}" 27 | print pom.version 28 | // execute the unit testing and collect the reports 29 | junit '**//*target/surefire-reports/TEST-*.xml' 30 | archive 'target*//*.jar' 31 | } else { 32 | bat(/"${mvnHome}\bin\mvn" -Dintegration-tests.skip=true clean package/) 33 | def pom = readMavenPom file: 'pom.xml' 34 | print pom.version 35 | junit '**//*target/surefire-reports/TEST-*.xml' 36 | archive 'target*//*.jar' 37 | } 38 | } 39 | 40 | } 41 | } 42 | stage('Integration tests') { 43 | // Run integration test 44 | steps { 45 | script { 46 | def mvnHome = tool 'Maven 3.5.2' 47 | if (isUnix()) { 48 | // just to trigger the integration test without unit testing 49 | sh "'${mvnHome}/bin/mvn' verify -Dunit-tests.skip=true" 50 | } else { 51 | bat(/"${mvnHome}\bin\mvn" verify -Dunit-tests.skip=true/) 52 | } 53 | 54 | } 55 | // cucumber reports collection 56 | cucumber buildStatus: null, fileIncludePattern: '**/cucumber.json', jsonReportDirectory: 'target', sortingMethod: 'ALPHABETICAL' 57 | } 58 | } 59 | stage('Sonar scan execution') { 60 | // Run the sonar scan 61 | steps { 62 | script { 63 | def mvnHome = tool 'Maven 3.5.2' 64 | withSonarQubeEnv { 65 | 66 | sh "'${mvnHome}/bin/mvn' verify sonar:sonar -Dintegration-tests.skip=true -Dmaven.test.failure.ignore=true" 67 | } 68 | } 69 | } 70 | } 71 | // waiting for sonar results based into the configured web hook in Sonar server which push the status back to jenkins 72 | stage('Sonar scan result check') { 73 | steps { 74 | timeout(time: 2, unit: 'MINUTES') { 75 | retry(3) { 76 | script { 77 | def qg = waitForQualityGate() 78 | if (qg.status != 'OK') { 79 | error "Pipeline aborted due to quality gate failure: ${qg.status}" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | stage('Development deploy approval and deployment') { 87 | steps { 88 | script { 89 | if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { 90 | timeout(time: 3, unit: 'MINUTES') { 91 | // you can use the commented line if u have specific user group who CAN ONLY approve 92 | //input message:'Approve deployment?', submitter: 'it-ops' 93 | input message: 'Approve deployment?' 94 | } 95 | timeout(time: 2, unit: 'MINUTES') { 96 | // 97 | if (developmentArtifactVersion != null && !developmentArtifactVersion.isEmpty()) { 98 | // replace it with your application name or make it easily loaded from pom.xml 99 | def jarName = "application-${developmentArtifactVersion}.jar" 100 | echo "the application is deploying ${jarName}" 101 | // NOTE : CREATE your deployemnt JOB, where it can take parameters whoch is the jar name to fetch from jenkins workspace 102 | build job: 'ApplicationToDev', parameters: [[$class: 'StringParameterValue', name: 'jarName', value: jarName]] 103 | echo 'the application is deployed !' 104 | } else { 105 | error 'the application is not deployed as development version is null!' 106 | } 107 | 108 | } 109 | } 110 | } 111 | } 112 | } 113 | stage('DEV sanity check') { 114 | steps { 115 | // give some time till the deployment is done, so we wait 45 seconds 116 | sleep(45) 117 | script { 118 | if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { 119 | timeout(time: 1, unit: 'MINUTES') { 120 | script { 121 | def mvnHome = tool 'Maven 3.5.2' 122 | //NOTE : if u change the sanity test class name , change it here as well 123 | sh "'${mvnHome}/bin/mvn' -Dtest=ApplicationSanityCheck_ITT surefire:test" 124 | } 125 | 126 | } 127 | } 128 | } 129 | } 130 | } 131 | stage('Release and publish artifact') { 132 | when { 133 | // check if branch is master 134 | branch 'master' 135 | } 136 | steps { 137 | // create the release version then create a tage with it , then push to nexus releases the released jar 138 | script { 139 | def mvnHome = tool 'Maven 3.5.2' // 140 | if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { 141 | def v = getReleaseVersion() 142 | releasedVersion = v; 143 | if (v) { 144 | echo "Building version ${v} - so released version is ${releasedVersion}" 145 | } 146 | // jenkins user credentials ID which is transparent to the user and password change 147 | sshagent(['0000000-3b5a-454e-a8e6-c6b6114d36000']) { 148 | sh "git tag -f v${v}" 149 | sh "git push -f --tags" 150 | } 151 | sh "'${mvnHome}/bin/mvn' -Dmaven.test.skip=true versions:set -DgenerateBackupPoms=false -DnewVersion=${v}" 152 | sh "'${mvnHome}/bin/mvn' -Dmaven.test.skip=true clean deploy" 153 | 154 | } else { 155 | error "Release is not possible. as build is not successful" 156 | } 157 | } 158 | } 159 | } 160 | stage('Deploy to Acceptance') { 161 | when { 162 | // check if branch is master 163 | branch 'master' 164 | } 165 | steps { 166 | script { 167 | if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { 168 | timeout(time: 3, unit: 'MINUTES') { 169 | //input message:'Approve deployment?', submitter: 'it-ops' 170 | input message: 'Approve deployment to UAT?' 171 | } 172 | timeout(time: 3, unit: 'MINUTES') { 173 | // deployment job which will take the relasesed version 174 | if (releasedVersion != null && !releasedVersion.isEmpty()) { 175 | // make the applciation name for the jar configurable 176 | def jarName = "application-${releasedVersion}.jar" 177 | echo "the application is deploying ${jarName}" 178 | // NOTE : DO NOT FORGET to create your UAT deployment jar , check Job AlertManagerToUAT in Jenkins for reference 179 | // the deployemnt should be based into Nexus repo 180 | build job: 'AApplicationToACC', parameters: [[$class: 'StringParameterValue', name: 'jarName', value: jarName], [$class: 'StringParameterValue', name: 'appVersion', value: releasedVersion]] 181 | echo 'the application is deployed !' 182 | } else { 183 | error 'the application is not deployed as released version is null!' 184 | } 185 | 186 | } 187 | } 188 | } 189 | } 190 | } 191 | stage('ACC E2E tests') { 192 | when { 193 | // check if branch is master 194 | branch 'master' 195 | } 196 | steps { 197 | // give some time till the deployment is done, so we wait 45 seconds 198 | sleep(45) 199 | script { 200 | if (currentBuild.result == null || currentBuild.result == 'SUCCESS') { 201 | timeout(time: 1, unit: 'MINUTES') { 202 | 203 | script { 204 | def mvnHome = tool 'Maven 3.5.2' 205 | // NOTE : if you change the test class name change it here as well 206 | sh "'${mvnHome}/bin/mvn' -Dtest=ApplicationE2E surefire:test" 207 | } 208 | 209 | } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | post { 216 | // Always runs. And it runs before any of the other post conditions. 217 | always { 218 | // Let's wipe out the workspace before we finish! 219 | deleteDir() 220 | } 221 | success { 222 | sendEmail("Successful"); 223 | } 224 | unstable { 225 | sendEmail("Unstable"); 226 | } 227 | failure { 228 | sendEmail("Failed"); 229 | } 230 | } 231 | 232 | // The options directive is for configuration that applies to the whole job. 233 | options { 234 | // For example, we'd like to make sure we only keep 10 builds at a time, so 235 | // we don't fill up our storage! 236 | buildDiscarder(logRotator(numToKeepStr: '5')) 237 | 238 | // And we'd really like to be sure that this build doesn't hang forever, so 239 | // let's time it out after an hour. 240 | timeout(time: 25, unit: 'MINUTES') 241 | } 242 | 243 | } 244 | def developmentArtifactVersion = '' 245 | def releasedVersion = '' 246 | // get change log to be send over the mail 247 | @NonCPS 248 | def getChangeString() { 249 | MAX_MSG_LEN = 100 250 | def changeString = "" 251 | 252 | echo "Gathering SCM changes" 253 | def changeLogSets = currentBuild.changeSets 254 | for (int i = 0; i < changeLogSets.size(); i++) { 255 | def entries = changeLogSets[i].items 256 | for (int j = 0; j < entries.length; j++) { 257 | def entry = entries[j] 258 | truncated_msg = entry.msg.take(MAX_MSG_LEN) 259 | changeString += " - ${truncated_msg} [${entry.author}]\n" 260 | } 261 | } 262 | 263 | if (!changeString) { 264 | changeString = " - No new changes" 265 | } 266 | return changeString 267 | } 268 | 269 | def sendEmail(status) { 270 | mail( 271 | to: "$EMAIL_RECIPIENTS", 272 | subject: "Build $BUILD_NUMBER - " + status + " (${currentBuild.fullDisplayName})", 273 | body: "Changes:\n " + getChangeString() + "\n\n Check console output at: $BUILD_URL/console" + "\n") 274 | } 275 | 276 | def getDevVersion() { 277 | def gitCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() 278 | def versionNumber; 279 | if (gitCommit == null) { 280 | versionNumber = env.BUILD_NUMBER; 281 | } else { 282 | versionNumber = gitCommit.take(8); 283 | } 284 | print 'build versions...' 285 | print versionNumber 286 | return versionNumber 287 | } 288 | 289 | def getReleaseVersion() { 290 | def pom = readMavenPom file: 'pom.xml' 291 | def gitCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() 292 | def versionNumber; 293 | if (gitCommit == null) { 294 | versionNumber = env.BUILD_NUMBER; 295 | } else { 296 | versionNumber = gitCommit.take(8); 297 | } 298 | return pom.version.replace("-SNAPSHOT", ".${versionNumber}") 299 | } 300 | 301 | // if you want parallel execution , check below : 302 | /* stage('Quality Gate(Integration Tests and Sonar Scan)') { 303 | // Run the maven build 304 | steps { 305 | parallel( 306 | IntegrationTest: { 307 | script { 308 | def mvnHome = tool 'Maven 3.5.2' 309 | if (isUnix()) { 310 | sh "'${mvnHome}/bin/mvn' verify -Dunit-tests.skip=true" 311 | } else { 312 | bat(/"${mvnHome}\bin\mvn" verify -Dunit-tests.skip=true/) 313 | } 314 | } 315 | }, 316 | SonarCheck: { 317 | script { 318 | def mvnHome = tool 'Maven 3.5.2' 319 | withSonarQubeEnv { 320 | // sh "'${mvnHome}/bin/mvn' verify sonar:sonar -Dsonar.host.url=http://bicsjava.bc/sonar/ -Dmaven.test.failure.ignore=true" 321 | sh "'${mvnHome}/bin/mvn' verify sonar:sonar -Dmaven.test.failure.ignore=true" 322 | } 323 | } 324 | }, 325 | failFast: true) 326 | } 327 | }*/ 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A spring boot 2 sample app project ![Twitter Follow](https://img.shields.io/twitter/follow/mromeh.svg?style=social) 2 | 3 | ## About 4 | 5 | Sample app generated from my spring boot custome maven archetype : https://github.com/Romeh/spring-boot-quickstart-archtype 6 | 7 | for more details about it , check my posts here : 8 | - https://mromeh.com/2017/12/04/spring-boot-integration-test-with-cucumber-and-jenkins-pipeline/ 9 | - https://mromeh.com/2017/12/04/spring-boot-with-embedded-config-server-via-spring-cloud-config/ 10 | - https://mromeh.com/2017/12/04/spring-boot-integration-test-with-cucumber-and-jenkins-pipeline/ 11 | 12 | ## Technical Stack 13 | 14 | - Java 1.8+ 15 | - Maven 3.5+ 16 | - Spring boot 2.2.2.RELEASE 17 | - Lombok abstraction 18 | - JPA with H2 for explanation 19 | - Swagger 2 API documentation 20 | - Spring retry and circuit breaker for external service call 21 | - REST API model validation 22 | - Spring cloud config for external configuration on GIT REPO 23 | - Cucumber and Spring Boot test for integration test 24 | - Jenkins Pipeline for multi branch project 25 | - Continuous delivery and integration standards with Sonar check and release management 26 | - Support retry in sanity checks 27 | 28 | ## Installation 29 | 30 | To run locally , you need to configure the run configuration by passing : 31 | - VM parameter: -DLOG_PATH=../log 32 | - Set SPRING profile to LOCAL 33 | 34 | Test on the browser via SWAGGER 35 | ------------------- 36 | 37 | ```sh 38 | http://localhost:8080/swagger-ui.html 39 | ``` 40 | 41 | ## License 42 | 43 | This software is licensed under the [BSD License][BSD]. For more information, read the file [LICENSE](LICENSE). 44 | 45 | [BSD]: https://opensource.org/licenses/BSD-3-Clause 46 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.test 6 | sampleapp 7 | 1.0.0-SNAPSHOT 8 | jar 9 | 10 | sampleapp 11 | sampleapp project for Spring Boot 12 | 13 | 14 | 15 | 16 | 1.8 17 | UTF-8 18 | UTF-8 19 | ${java.version} 20 | ${java.version} 21 | false 22 | false 23 | Finchley.RELEASE 24 | 2.2.2.RELEASE 25 | ${project.artifactId} 26 | LOCAL 27 | false 28 | false 29 | jacoco 30 | reuseReports 31 | ${project.basedir}/../target/jacoco.exec 32 | java 33 | 34 | **/domain/* 35 | 36 | 0 37 | ${project.basedir}/../target/jacoco.exec 38 | ${project.basedir}/../target/reports/jacoco 39 | true 40 | 1.2.5.RELEASE 41 | 2.8.47 42 | 2.9.2 43 | 1.2.3 44 | 1.2.6 45 | 2.3.6 46 | 1.18.10 47 | 48 | com.test.sampleapp.web 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-parent 57 | ${spring.boot.version} 58 | pom 59 | import 60 | 61 | 62 | org.springframework.cloud 63 | spring-cloud-dependencies 64 | ${spring-cloud.version} 65 | pom 66 | import 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-actuator 77 | 78 | 79 | org.springframework.cloud 80 | spring-cloud-config-server 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-web 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-aop 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-data-jpa 93 | 94 | 95 | 96 | com.zaxxer 97 | HikariCP 98 | 99 | 100 | 101 | 102 | com.h2database 103 | h2 104 | runtime 105 | 106 | 107 | org.projectlombok 108 | lombok 109 | ${lombok-version} 110 | true 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-starter-test 115 | test 116 | 117 | 118 | io.springfox 119 | springfox-swagger2 120 | ${swagger.version} 121 | 122 | 123 | 124 | io.springfox 125 | springfox-swagger-ui 126 | ${swagger.version} 127 | 128 | 129 | 130 | ch.qos.logback 131 | logback-core 132 | ${logback.version} 133 | runtime 134 | 135 | 136 | ch.qos.logback 137 | logback-classic 138 | ${logback.version} 139 | runtime 140 | 141 | 142 | org.mockito 143 | mockito-core 144 | ${mockito-core.version} 145 | test 146 | 147 | 148 | 149 | info.cukes 150 | cucumber-java 151 | ${cucumber-version} 152 | test 153 | 154 | 155 | info.cukes 156 | cucumber-junit 157 | ${cucumber-version} 158 | test 159 | 160 | 161 | info.cukes 162 | cucumber-spring 163 | ${cucumber-version} 164 | test 165 | 166 | 167 | org.springframework.retry 168 | spring-retry 169 | ${spring.retry.version} 170 | 171 | 172 | org.modelmapper 173 | modelmapper 174 | ${modelmapper-version} 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ${project.artifact.name}-${project.version}-${build.number} 183 | 184 | 185 | org.springframework.boot 186 | spring-boot-maven-plugin 187 | 188 | -Xmx512m -XX:MaxDirectMemorySize=1200m 189 | 190 | 191 | 192 | 193 | build-info 194 | 195 | 196 | 197 | 198 | 199 | 200 | maven-failsafe-plugin 201 | 202 | ${integration-tests.skip} 203 | 204 | **/*IT.java 205 | 206 | 207 | 208 | 209 | 210 | integration-test 211 | verify 212 | 213 | 214 | 215 | 216 | 217 | 218 | maven-surefire-plugin 219 | 2.20 220 | 221 | ${unit-tests.skip} 222 | 223 | **/*IT.java 224 | 225 | 226 | 227 | 228 | 229 | org.jacoco 230 | jacoco-maven-plugin 231 | 0.7.9 232 | 233 | ${sonar.jacoco.reportPaths} 234 | true 235 | 236 | 237 | 238 | default-prepare-agent 239 | 240 | prepare-agent 241 | 242 | 243 | 244 | default-report 245 | 246 | report 247 | 248 | 249 | ${jacoco.reporting.outputDirectory} 250 | 251 | 252 | 253 | default-report-aggregate 254 | verify 255 | 256 | report-aggregate 257 | 258 | 259 | 260 | ${jacoco.dataFile} 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | com.github.kongchen 269 | swagger-maven-plugin 270 | 3.1.0 271 | 272 | 273 | 274 | true 275 | ${rest-locations} 276 | service.url:8080 277 | / 278 | 279 | Application manager swagger 280 | 0.0.1 281 | 282 | mahmoud.romeh@test.com 283 | Mahmoud Romih 284 | 285 | 286 | http://www.apache.org/licenses/LICENSE-2.0.html 287 | Apache 2.0 288 | 289 | 290 | ${project.build.directory}/generated/swagger-ui 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | compile 299 | 300 | generate 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | -------------------------------------------------------------------------------- /sampleapp.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/Application.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp; 2 | 3 | import java.util.Collections; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.context.ApplicationPidFileWriter; 8 | import org.springframework.retry.annotation.EnableRetry; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | 11 | @SpringBootApplication 12 | @EnableScheduling 13 | @EnableRetry 14 | public class Application { 15 | 16 | public static void main(String[] args) { 17 | final SpringApplication springApplication = 18 | new SpringApplication(Application.class); 19 | // it is being added here for LOCAL run ONLY , spring profiles should be be run time parameters when run spring boot jar 20 | springApplication.setDefaultProperties(Collections.singletonMap("spring.profiles.default","LOCAL")); 21 | springApplication.addListeners(new ApplicationPidFileWriter()); 22 | springApplication.run(args); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.PathSelectors; 6 | import springfox.documentation.builders.RequestHandlerSelectors; 7 | import springfox.documentation.service.ApiInfo; 8 | import springfox.documentation.service.Contact; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 12 | 13 | import java.util.Collections; 14 | 15 | /** 16 | * Configuration class which enables Swagger 17 | * 18 | * @author romih 19 | */ 20 | @Configuration 21 | @EnableSwagger2 22 | public class SwaggerConfiguration { 23 | 24 | @Bean 25 | public Docket api() { 26 | return new Docket(DocumentationType.SWAGGER_2) 27 | .select() 28 | .apis(RequestHandlerSelectors.any()) 29 | .paths(PathSelectors.any()) 30 | .build() 31 | .apiInfo(apiInfo()); 32 | } 33 | 34 | private ApiInfo apiInfo() { 35 | ApiInfo apiInfo = new ApiInfo( 36 | "Application REST API", 37 | "Application manager REST API documentation.", 38 | "API 1.0", 39 | "Terms of service based into company terms of use", 40 | new Contact("yourCompany", null, "test@test.com"), 41 | "License of API for YourCompany use only", null, Collections.emptyList()); 42 | return apiInfo; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/async/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Async Helpers 3 | */ 4 | package com.test.sampleapp.async; -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.config; 2 | 3 | import org.modelmapper.ModelMapper; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * Created by MRomeh 9 | */ 10 | @Configuration 11 | public class ApplicationConfig { 12 | @Bean 13 | ModelMapper modelMapper() { 14 | return new ModelMapper(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/config/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Spring Framework Configuration Files 3 | */ 4 | package com.test.sampleapp.config; -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/domain/ApplicationItem.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.domain; 2 | 3 | import java.io.Serializable; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.*; 6 | import javax.persistence.*; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by MRomeh 11 | */ 12 | @Entity 13 | @Table(name = "Applications") 14 | @Builder 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class ApplicationItem implements Serializable { 19 | @Column(name = "alertContent", nullable = false) 20 | @ElementCollection(targetClass = String.class) 21 | private Map applicationContent; 22 | @Column(name = "applicationCode", nullable = false) 23 | private String applicationCode; 24 | @Id 25 | @GeneratedValue 26 | @ApiModelProperty(notes = "the auto internal generated id by alert manager DB , not required to be entered by user into REST API ") 27 | private Long id; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/domain/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * JPA Domain Objects 3 | */ 4 | package com.test.sampleapp.domain; -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/exceptions/NotAllowedOperationException.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.exceptions; 2 | 3 | /** 4 | * This exception should be thrown in all cases when a resource cannot be found 5 | * 6 | * @author romih 7 | */ 8 | public class NotAllowedOperationException extends RuntimeException { 9 | 10 | /** 11 | * 12 | * @param message the message 13 | */ 14 | public NotAllowedOperationException(final String message) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/exceptions/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.exceptions; 2 | 3 | /** 4 | * This exception should be thrown in all cases when a resource cannot be found 5 | * 6 | * @author romih 7 | */ 8 | public class ResourceNotFoundException extends RuntimeException { 9 | 10 | /** 11 | * 12 | * @param message the message 13 | */ 14 | public ResourceNotFoundException(final String message) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/exceptions/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Spring Security Configuration 3 | */ 4 | package com.test.sampleapp.exceptions; 5 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/repository/ApplicationRepository.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.repository; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | import com.test.sampleapp.domain.ApplicationItem; 7 | import java.util.List; 8 | 9 | /** 10 | * Created by MRomeh on 23/08/2017. 11 | */ 12 | @Repository 13 | public interface ApplicationRepository extends JpaRepository { 14 | 15 | List findApplicationEntriesByApplicationCode(String applicationCode); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/repository/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Spring Data JPA Repositories 3 | */ 4 | package com.test.sampleapp.repository; -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/service/ApplicationService.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import com.test.sampleapp.repository.ApplicationRepository; 9 | import com.test.sampleapp.domain.ApplicationItem; 10 | import java.util.List; 11 | 12 | 13 | /** 14 | * Created by MRomeh 15 | */ 16 | @Service 17 | public class ApplicationService { 18 | private static final Logger log = LoggerFactory.getLogger(ApplicationService.class); 19 | @Autowired 20 | private ApplicationRepository applicationRepository; 21 | 22 | @Transactional 23 | public void createApplicationItem(ApplicationItem applicationItem) { 24 | applicationRepository.save(applicationItem); 25 | } 26 | 27 | public List getApplicationItems() { 28 | return applicationRepository.findAll(); 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/service/ExternalServiceWithSpringRetryAndCircuitBreaker.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.service; 2 | 3 | import java.util.concurrent.TimeoutException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.retry.annotation.CircuitBreaker; 7 | import org.springframework.retry.annotation.Recover; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | /** 11 | * Created by MRomeh 12 | */ 13 | @Service 14 | public class ExternalServiceWithSpringRetryAndCircuitBreaker { 15 | private static final Logger log = LoggerFactory.getLogger(ExternalServiceWithSpringRetryAndCircuitBreaker.class); 16 | 17 | /* example of circuit breaker with spring retry which will retry to call the server 2 times in case of error 18 | and for example exclude Timeout exception from retry conditions and go to recover directly 19 | */ 20 | @CircuitBreaker(maxAttempts = 2, openTimeout = 5000l, resetTimeout = 10000l, exclude = TimeoutException.class) 21 | public void sendEmail() { 22 | // add your external service call here so it can be protected by Spring rety and CircuitBreaker logic 23 | 24 | } 25 | 26 | /** 27 | * The recover method needs to have same return type and parameters which will be called in case the circuit is closed or retrials are over 28 | * so this the fallback logic 29 | * 30 | * @return 31 | */ 32 | @Recover 33 | private void fallbackForCall() { 34 | log.error("Fallback for external service call invoked, the external service is NOT reachable"); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/service/ScheduledServiceAction.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | 8 | 9 | 10 | /** 11 | * Created by MRomeh 12 | */ 13 | @Service 14 | public class ScheduledServiceAction { 15 | private static final Logger log = LoggerFactory.getLogger(ScheduledServiceAction.class); 16 | 17 | 18 | @Scheduled(initialDelayString = "${initialDelay}", fixedDelayString = "${fixedDelay}") 19 | public void cleanExpiredRecords() { 20 | log.debug("Starting the clean up job to clear the expired records"); 21 | // do your scheduled action which is configured based into the above properties 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/service/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Service Layer Beans 3 | */ 4 | package com.test.sampleapp.service; -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/ApplicationController.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.web; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import org.modelmapper.ModelMapper; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.*; 11 | import com.test.sampleapp.web.dto.*; 12 | import javax.validation.Valid; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | import com.test.sampleapp.service.ApplicationService; 16 | import com.test.sampleapp.domain.ApplicationItem; 17 | /** 18 | * Created by MRomeh on 08/08/2017. 19 | */ 20 | @RestController 21 | @RequestMapping("/application") 22 | @Api(value = "Applciation demo") 23 | public class ApplicationController { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(ApplicationController.class); 26 | 27 | @Autowired 28 | private ModelMapper modelMapper; 29 | 30 | @Autowired 31 | private ApplicationService applicationService; 32 | 33 | 34 | @RequestMapping(method = RequestMethod.GET, produces = "application/json") 35 | @ResponseBody 36 | @ApiOperation(value = "view the list of ALL current active created stored appllication items", response = ApplicationEntry.class) 37 | public List getAllAlerts() { 38 | log.debug("Trying to retrieve all alerts"); 39 | return applicationService.getApplicationItems().stream() 40 | .map(applicationItem -> modelMapper.map(applicationItem, ApplicationEntry.class)).collect(Collectors.toList()); 41 | 42 | } 43 | 44 | 45 | @RequestMapping(method = RequestMethod.POST, produces = "application/json") 46 | @ResponseBody 47 | @ResponseStatus(HttpStatus.CREATED) 48 | @ApiOperation(value = "Create an application entry into the application manager") 49 | public void createAlert(@Valid @RequestBody ApplicationEntry request) { 50 | log.debug("Trying to create an alert: {}", request.toString()); 51 | applicationService.createApplicationItem(modelMapper.map(request, ApplicationItem.class)); 52 | } 53 | 54 | @RequestMapping(method={RequestMethod.GET},value={"/version"}) 55 | public String getVersion() { 56 | return "1.0"; 57 | } 58 | 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.web; 2 | 3 | import com.test.sampleapp.web.dto.CustomErrorResponse; 4 | import org.slf4j.Logger; 5 | import com.test.sampleapp.exceptions.*; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | 13 | /** 14 | * Generic error handling mechanism. 15 | * 16 | * @author romih 17 | */ 18 | @ControllerAdvice 19 | public class ErrorHandler { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(ErrorHandler.class); 22 | 23 | @ResponseStatus(HttpStatus.NOT_FOUND) // 404 24 | @ExceptionHandler(ResourceNotFoundException.class) 25 | @ResponseBody 26 | public CustomErrorResponse handleNotFound(ResourceNotFoundException ex) { 27 | log.warn("Entity was not found", ex); 28 | return new CustomErrorResponse(ERROR_CODE.E0001.name(), ex.getMessage()); 29 | } 30 | 31 | 32 | @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 33 | @ExceptionHandler(NotAllowedOperationException.class) 34 | @ResponseBody 35 | public CustomErrorResponse handleNotFound(NotAllowedOperationException ex) { 36 | log.warn("Not Allowed operation", ex); 37 | return new CustomErrorResponse(ERROR_CODE.E0002.name(), ex.getMessage()); 38 | } 39 | 40 | private enum ERROR_CODE { 41 | E0001, E0002 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/dto/ApplicationEntry.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.web.dto; 2 | 3 | import java.util.Map; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.*; 6 | 7 | 8 | /** 9 | * Created by MRomeh 10 | */ 11 | 12 | @Builder 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class ApplicationEntry { 17 | @ApiModelProperty(notes = "the key value alert content for application item description required to be entered by user into REST API ", required = true) 18 | private Map applicationContent; 19 | @ApiModelProperty(notes = "Applciation code required to be entered by user into REST API ", required = true) 20 | private String applicationCode; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/dto/CustomErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.web.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Necessary for proper Swagger documentation. 9 | * 10 | * @author romih 11 | */ 12 | @SuppressWarnings("unused") 13 | @AllArgsConstructor 14 | @Getter 15 | public class CustomErrorResponse implements Serializable { 16 | 17 | private static final long serialVersionUID = -7755563009111273632L; 18 | 19 | private String errorCode; 20 | 21 | private String errorMessage; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/dto/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Service Layer Beans 3 | */ 4 | package com.test.sampleapp.web.dto; 5 | -------------------------------------------------------------------------------- /src/main/java/com/test/sampleapp/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Service Layer Beans 3 | */ 4 | package com.test.sampleapp.web; 5 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jackson: 3 | default-property-inclusion: non_null 4 | jpa: 5 | hibernate: 6 | ddl-auto: update 7 | datasource: 8 | tomcat: 9 | max-active: 10 10 | max-idle: 10000 11 | max-wait: 10000 12 | test-on-borrow: true 13 | url: jdbc:h2:file:${dataBaseUrl} 14 | data-username: sa 15 | data-password: 16 | driver-class-name: org.h2.Driver 17 | 18 | dataBaseUrl: ./h2/alerts_db;DB_CLOSE_ON_EXIT=FALSE 19 | 20 | 21 | initialDelay: 10000 22 | fixedDelay: 60000 23 | 24 | server: 25 | tomcat: 26 | accessLog.enabled: true 27 | basedir: "${LOG_PATH}/Application" 28 | accessLogPattern: "%h %l %u %t \"%r\" %s %b %D" 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | # change the application name for your project name , this name is reserved for the maven archetype code generation 3 | application: 4 | name: Application 5 | 6 | --- 7 | # DO NOT FORGET TO ADD YOUR YAML CONFIG FILE IN config server as shown below 8 | spring: 9 | cloud: 10 | config: 11 | failFast: true 12 | server: 13 | bootstrap: true 14 | git: 15 | uri: https://github.com/Romeh/config.git 16 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${FILE_LOG_PATTERN} 10 | utf8 11 | 12 | 13 | 14 | 15 | ${LOG_HOME}/Application.log 16 | true 17 | 18 | ${FILE_LOG_PATTERN} 19 | 20 | 21 | Application.log.%i 22 | 23 | 25 | 10MB 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/acceptance/ApplicationE2E.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.acceptance; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 7 | import org.springframework.web.client.RestTemplate; 8 | import java.net.InetSocketAddress; 9 | import java.net.Proxy; 10 | import java.net.URL; 11 | import com.test.sampleapp.retry.Retry; 12 | 13 | import static org.junit.Assert.assertTrue; 14 | 15 | 16 | public class ApplicationE2E{ 17 | private int port = 8080; 18 | private RestTemplate restTemplate; 19 | private URL baseURL; 20 | 21 | @Before 22 | public void setUp() throws Exception { 23 | // replace that with UAT server host 24 | this.baseURL = new URL("http://localhost:" + port + "/"); 25 | // disabled proxy config to run locally 26 | SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); 27 | // just added for showing how to configure the proxy 28 | //Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("userproxy.glb.ebc.local", 8080)); 29 | //requestFactory.setProxy(proxy); 30 | restTemplate = new RestTemplate(); 31 | 32 | 33 | } 34 | // example of true end to end call which call UAT real endpoint 35 | @Test 36 | public void test_is_server_up() { 37 | assertTrue(restTemplate.getForEntity(baseURL + "/actuator/health", String.class).getStatusCode().is2xxSuccessful()); 38 | 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/integration/CucumberIntegrationIT.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.integration; 2 | 3 | import cucumber.api.CucumberOptions; 4 | import cucumber.api.junit.Cucumber; 5 | import org.junit.runner.RunWith; 6 | 7 | /** 8 | * Created by MRomeh. 9 | */ 10 | @RunWith(Cucumber.class) 11 | @CucumberOptions(features = {"src/test/resources/features"}, format = {"pretty", "html:target/reports/cucumber/html", 12 | "json:target/cucumber.json", "usage:target/usage.jsonx", "junit:target/junit.xml"}) 13 | public class CucumberIntegrationIT { 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/integration/CucumberRoot.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.integration; 2 | 3 | import org.junit.Before; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.mock.mockito.MockBean; 7 | import org.springframework.boot.test.web.client.TestRestTemplate; 8 | import org.springframework.test.context.ActiveProfiles; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import com.test.sampleapp.Application; 11 | import java.util.Collections; 12 | 13 | /** 14 | * Created by MRomeh. 15 | */ 16 | 17 | 18 | @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 19 | @ActiveProfiles("INTEGRATION_TEST") 20 | @ContextConfiguration 21 | public class CucumberRoot { 22 | 23 | @Autowired 24 | protected TestRestTemplate template; 25 | 26 | @Before 27 | public void before() { 28 | // demo to show how to add custom header Globally for the http request in spring test template , like IV user header 29 | template.getRestTemplate().setInterceptors(Collections.singletonList((request, body, execution) -> { 30 | request.getHeaders() 31 | .add("iv-user", "user"); 32 | return execution.execute(request, body); 33 | })); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/integration/GetHealthStep.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.integration; 2 | 3 | import cucumber.api.java.en.Then; 4 | import cucumber.api.java.en.When; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | 8 | import static org.hamcrest.Matchers.is; 9 | import static org.junit.Assert.assertThat; 10 | 11 | /** 12 | * Created by MRomeh. 13 | */ 14 | public class GetHealthStep extends CucumberRoot { 15 | private ResponseEntity response; // output 16 | 17 | @When("^the client calls /health$") 18 | public void the_client_issues_GET_health() throws Throwable { 19 | response = template.getForEntity("/actuator/health", String.class); 20 | } 21 | 22 | @Then("^the client receives response status code of (\\d+)$") 23 | public void the_client_receives_status_code_of(int statusCode) throws Throwable { 24 | HttpStatus currentStatusCode = response.getStatusCode(); 25 | assertThat("status code is incorrect : " + 26 | response.getBody(), currentStatusCode.value(), is(statusCode)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/integration/GetVersionStep.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.integration; 2 | 3 | import cucumber.api.java.en.And; 4 | import cucumber.api.java.en.Then; 5 | import cucumber.api.java.en.When; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | 9 | import static org.hamcrest.Matchers.is; 10 | import static org.junit.Assert.assertThat; 11 | 12 | /** 13 | * Created by MRomeh. 14 | */ 15 | public class GetVersionStep extends CucumberRoot { 16 | private ResponseEntity response; // output 17 | 18 | @When("^the client calls /version$") 19 | public void the_client_issues_GET_version() throws Throwable { 20 | response = template.getForEntity("/application/version", String.class); 21 | } 22 | 23 | @Then("^the client receives status code of (\\d+)$") 24 | public void the_client_receives_status_code_of(int statusCode) throws Throwable { 25 | HttpStatus currentStatusCode = response.getStatusCode(); 26 | assertThat("status code is incorrect : " + 27 | response.getBody(), currentStatusCode.value(), is(statusCode)); 28 | } 29 | 30 | @And("^the client receives server version (.+)$") 31 | public void the_client_receives_server_version_body(String version) throws Throwable { 32 | assertThat(response.getBody(), is(version)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/retry/Retry.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.retry; 2 | 3 | /** 4 | * Created by MRomeh on 05/09/2017. 5 | */ 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | /** 13 | * Retries a unit-test according to the attributes set here 14 | *

15 | * The class containing the test(s) decorated with this annotation must have a public field of type {@link RetryRule} 16 | */ 17 | @Retention(RetentionPolicy.RUNTIME) 18 | @Target({ElementType.METHOD, ElementType.TYPE}) 19 | public @interface Retry { 20 | /** 21 | * @return the number of times to try this method before the failure is propagated through 22 | */ 23 | int times() default 3; 24 | 25 | /** 26 | * @return how long to sleep between invocations of the unit tests, in milliseconds 27 | */ 28 | long timeout() default 0; 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/retry/RetryException.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.retry; 2 | 3 | /** 4 | * Created by MRomeh on 05/09/2017. 5 | */ 6 | 7 | import javax.validation.constraints.NotNull; 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | import com.test.sampleapp.retry.*; 11 | 12 | /** 13 | * An exception thrown to signal that a retry operation (executed via {@link RetryRule}) has retried more than the 14 | * allowed number of times, and has still failed. 15 | */ 16 | public final class RetryException extends RuntimeException { 17 | 18 | private RetryException(@NotNull String message) { 19 | super(message); 20 | } 21 | 22 | /** 23 | * @param errors the errors for each attempt at running this test-case 24 | */ 25 | @NotNull 26 | public static RetryException from(@NotNull Throwable[] errors) { 27 | final StringBuilder msg = new StringBuilder("Invoked methods still failed after " + errors.length + " attempts."); 28 | for (int i = 0; i < errors.length; i++) { 29 | final Throwable error = errors[i]; 30 | msg.append('\n'); 31 | msg.append("Attempt #").append(i).append(" threw exception:"); 32 | msg.append(stackTraceAsString(error)); 33 | } 34 | return new RetryException(msg.toString()); 35 | } 36 | 37 | @NotNull 38 | private static String stackTraceAsString(@NotNull Throwable t) { 39 | final StringWriter errors = new StringWriter(); 40 | t.printStackTrace(new PrintWriter(errors)); 41 | return errors.toString(); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/retry/RetryRule.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.retry; 2 | 3 | /** 4 | * Created by MRomeh on 05/09/2017. 5 | */ 6 | 7 | import org.junit.rules.TestRule; 8 | import org.junit.runner.Description; 9 | import org.junit.runners.model.Statement; 10 | import com.test.sampleapp.retry.*; 11 | import javax.validation.constraints.NotNull; 12 | import java.util.Arrays; 13 | 14 | public final class RetryRule implements TestRule { 15 | 16 | @NotNull 17 | private Throwable[] errors = new Throwable[0]; 18 | 19 | private int currentAttempt = 0; 20 | 21 | @Override 22 | public Statement apply(final Statement base, final Description description) { 23 | final Retry retryAnnotation = description.getAnnotation(Retry.class); 24 | if (retryAnnotation == null) { 25 | return base; 26 | } 27 | final int times = retryAnnotation.times(); 28 | if (times <= 0) { 29 | throw new IllegalArgumentException( 30 | "@" + Retry.class.getSimpleName() + " cannot be used with a \"times\" parameter less than 1" 31 | ); 32 | } 33 | final long timeout = retryAnnotation.timeout(); 34 | if (timeout < 0) { 35 | throw new IllegalArgumentException( 36 | "@" + Retry.class.getSimpleName() + " cannot be used with a \"timeout\" parameter less than 0" 37 | ); 38 | } 39 | 40 | errors = new Throwable[times]; 41 | 42 | return new Statement() { 43 | @Override 44 | public void evaluate() throws Throwable { 45 | while (currentAttempt < times) { 46 | try { 47 | base.evaluate(); 48 | return; 49 | } catch (Throwable t) { 50 | errors[currentAttempt] = t; 51 | currentAttempt++; 52 | Thread.sleep(timeout); 53 | } 54 | } 55 | throw RetryException.from(errors); 56 | } 57 | }; 58 | } 59 | 60 | /** 61 | * @return an array representing the errors that have been encountered so far. {@code errors()[0]} corresponds to the 62 | * Throwable encountered when running the test-case for the first time, {@code errors()[1]} corresponds to the 63 | * Throwable encountered when running the test-case for the second time, and so on. 64 | */ 65 | @NotNull 66 | public Throwable[] errors() { 67 | return Arrays.copyOfRange(errors, 0, currentAttempt); 68 | } 69 | 70 | /** 71 | * A convenience method to return the {@link Throwable} that was encountered on the last invocation of this test-case. 72 | * Returns {@code null} if this is the first invocation of the test-case. 73 | */ 74 | public Throwable lastError() { 75 | final int currentAttempt = currentAttempt(); 76 | final Throwable[] errors = errors(); 77 | if (currentAttempt == 0) { 78 | return null; 79 | } 80 | return errors[currentAttempt - 1]; 81 | } 82 | 83 | /** 84 | * @return the current attempt (0-indexed). 0 is the very first attempt, 1 is the next one, and so on. 85 | */ 86 | public int currentAttempt() { 87 | return currentAttempt; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/test/sampleapp/sanity/ApplicationSanityCheck_ITT.java: -------------------------------------------------------------------------------- 1 | package com.test.sampleapp.sanity; 2 | 3 | 4 | import com.test.sampleapp.retry.RetryRule; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 9 | import org.springframework.web.client.RestTemplate; 10 | import java.net.InetSocketAddress; 11 | import java.net.Proxy; 12 | import java.net.URL; 13 | import com.test.sampleapp.retry.Retry; 14 | 15 | import static org.junit.Assert.assertTrue; 16 | 17 | 18 | public class ApplicationSanityCheck_ITT { 19 | @Rule 20 | public final RetryRule retry = new RetryRule(); 21 | private int port = 8080; 22 | private RestTemplate template; 23 | private URL base; 24 | 25 | @Before 26 | public void setUp() throws Exception { 27 | this.base = new URL("http://localhost:" + port + "/"); 28 | // disabled proxy config to run locally 29 | SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); 30 | // just added for showing how to configure the proxy 31 | // Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("userproxy.glb.ebc.local", 8080)); 32 | //requestFactory.setProxy(proxy); 33 | template = new RestTemplate(requestFactory); 34 | 35 | 36 | } 37 | 38 | // and retry in case of failure 3 times with 20 seconds delay between each try 39 | @Test 40 | @Retry(times = 3, timeout = 20000) 41 | public void test_is_server_up() { 42 | assertTrue(template.getForEntity(base + "/actuator/health", String.class).getStatusCode().is2xxSuccessful()); 43 | 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/resources/features/health.feature: -------------------------------------------------------------------------------- 1 | Feature: the health can be retrieved 2 | Scenario: client makes call to GET /health 3 | When the client calls /health 4 | Then the client receives response status code of 200 -------------------------------------------------------------------------------- /src/test/resources/features/version.feature: -------------------------------------------------------------------------------- 1 | Feature: the version can be retrieved 2 | Scenario: client makes call to GET /version 3 | When the client calls /version 4 | Then the client receives status code of 200 5 | And the client receives server version 1.0 -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${FILE_LOG_PATTERN} 10 | utf8 11 | 12 | 13 | 14 | 15 | ${LOG_HOME}/application.log 16 | true 17 | 18 | ${FILE_LOG_PATTERN} 19 | 20 | 21 | application.log.%i 22 | 23 | 25 | 10MB 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /target/classes/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jackson: 3 | default-property-inclusion: non_null 4 | jpa: 5 | hibernate: 6 | ddl-auto: update 7 | datasource: 8 | tomcat: 9 | max-active: 10 10 | max-idle: 10000 11 | max-wait: 10000 12 | test-on-borrow: true 13 | url: jdbc:h2:file:${dataBaseUrl} 14 | data-username: sa 15 | data-password: 16 | driver-class-name: org.h2.Driver 17 | 18 | dataBaseUrl: ./h2/alerts_db;DB_CLOSE_ON_EXIT=FALSE 19 | 20 | 21 | initialDelay: 10000 22 | fixedDelay: 60000 23 | 24 | server: 25 | tomcat: 26 | accessLog.enabled: true 27 | basedir: "${LOG_PATH}/Application" 28 | accessLogPattern: "%h %l %u %t \"%r\" %s %b %D" 29 | 30 | 31 | -------------------------------------------------------------------------------- /target/classes/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | # change the application name for your project name , this name is reserved for the maven archetype code generation 3 | application: 4 | name: Application 5 | 6 | --- 7 | # DO NOT FORGET TO ADD YOUR YAML CONFIG FILE IN config server as shown below 8 | spring: 9 | cloud: 10 | config: 11 | failFast: true 12 | server: 13 | bootstrap: true 14 | git: 15 | uri: https://github.com/Romeh/config.git 16 | -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/Application.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/Application.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/SwaggerConfiguration.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/SwaggerConfiguration.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/config/ApplicationConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/config/ApplicationConfig.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/domain/ApplicationItem$ApplicationItemBuilder.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/domain/ApplicationItem$ApplicationItemBuilder.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/domain/ApplicationItem.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/domain/ApplicationItem.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/exceptions/NotAllowedOperationException.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/exceptions/NotAllowedOperationException.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/exceptions/ResourceNotFoundException.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/exceptions/ResourceNotFoundException.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/repository/ApplicationRepository.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/repository/ApplicationRepository.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/service/ApplicationService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/service/ApplicationService.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/service/ExternalServiceWithSpringRetryAndCircuitBreaker.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/service/ExternalServiceWithSpringRetryAndCircuitBreaker.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/service/ScheduledServiceAction.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/service/ScheduledServiceAction.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/ApplicationController.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/ApplicationController.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/ErrorHandler$ERROR_CODE.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/ErrorHandler$ERROR_CODE.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/ErrorHandler.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/ErrorHandler.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/dto/ApplicationEntry$ApplicationEntryBuilder.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/dto/ApplicationEntry$ApplicationEntryBuilder.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/dto/ApplicationEntry.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/dto/ApplicationEntry.class -------------------------------------------------------------------------------- /target/classes/com/test/sampleapp/web/dto/CustomErrorResponse.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/classes/com/test/sampleapp/web/dto/CustomErrorResponse.class -------------------------------------------------------------------------------- /target/classes/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${FILE_LOG_PATTERN} 10 | utf8 11 | 12 | 13 | 14 | 15 | ${LOG_HOME}/Application.log 16 | true 17 | 18 | ${FILE_LOG_PATTERN} 19 | 20 | 21 | Application.log.%i 22 | 23 | 25 | 10MB 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/acceptance/ApplicationE2E.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/acceptance/ApplicationE2E.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/integration/CucumberIntegrationIT.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/integration/CucumberIntegrationIT.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/integration/CucumberRoot.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/integration/CucumberRoot.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/integration/GetHealthStep.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/integration/GetHealthStep.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/integration/GetVersionStep.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/integration/GetVersionStep.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/retry/Retry.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/retry/Retry.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/retry/RetryException.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/retry/RetryException.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/retry/RetryRule$1.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/retry/RetryRule$1.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/retry/RetryRule.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/retry/RetryRule.class -------------------------------------------------------------------------------- /target/test-classes/com/test/sampleapp/sanity/ApplicationSanityCheck_ITT.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-sample-app/6f4168d091228891e74d3a4894acec38b9008f77/target/test-classes/com/test/sampleapp/sanity/ApplicationSanityCheck_ITT.class -------------------------------------------------------------------------------- /target/test-classes/features/health.feature: -------------------------------------------------------------------------------- 1 | Feature: the health can be retrieved 2 | Scenario: client makes call to GET /health 3 | When the client calls /health 4 | Then the client receives response status code of 200 -------------------------------------------------------------------------------- /target/test-classes/features/version.feature: -------------------------------------------------------------------------------- 1 | Feature: the version can be retrieved 2 | Scenario: client makes call to GET /version 3 | When the client calls /version 4 | Then the client receives status code of 200 5 | And the client receives server version 1.0 -------------------------------------------------------------------------------- /target/test-classes/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${FILE_LOG_PATTERN} 10 | utf8 11 | 12 | 13 | 14 | 15 | ${LOG_HOME}/application.log 16 | true 17 | 18 | ${FILE_LOG_PATTERN} 19 | 20 | 21 | application.log.%i 22 | 23 | 25 | 10MB 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------