├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── etc ├── checkstyle │ └── rules.xml ├── eclipse │ └── java-formatter.xml └── pmd │ └── ruleset.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── example │ │ └── ws │ │ ├── Application.java │ │ ├── actuator │ │ └── health │ │ │ └── GreetingHealthIndicator.java │ │ ├── batch │ │ └── GreetingBatchBean.java │ │ ├── model │ │ └── Greeting.java │ │ ├── repository │ │ └── GreetingRepository.java │ │ ├── service │ │ ├── EmailService.java │ │ ├── EmailServiceBean.java │ │ ├── GreetingService.java │ │ ├── GreetingServiceBean.java │ │ └── quote │ │ │ └── tss │ │ │ ├── Quote.java │ │ │ ├── QuoteResponse.java │ │ │ ├── QuoteResponseContents.java │ │ │ ├── QuoteResponseSuccess.java │ │ │ ├── QuoteService.java │ │ │ └── QuoteServiceBean.java │ │ ├── util │ │ └── AsyncResponse.java │ │ └── web │ │ ├── DefaultExceptionAttributes.java │ │ ├── ExceptionAttributes.java │ │ └── api │ │ ├── BaseController.java │ │ ├── GreetingController.java │ │ └── QuoteController.java └── resources │ ├── config │ ├── application-batch.properties │ └── application.properties │ └── data │ └── hsqldb │ ├── data.sql │ └── schema.sql └── test └── java └── org └── example └── ws ├── AbstractControllerTest.java ├── AbstractTest.java ├── service └── GreetingServiceTest.java └── web └── api ├── GreetingControllerMocksTest.java └── GreetingControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse Directories and Files 2 | .project 3 | .classpath 4 | .springBeans 5 | /.settings 6 | /.gradle 7 | 8 | # Generated Directories and Files 9 | /target 10 | /build 11 | /bin 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Fundamentals 2 | 3 | ## Acknowledgements 4 | 5 | This is a [LEAN**STACKS**](http://www.leanstacks.com) solution. 6 | 7 | For more detailed information and instruction about constructing Spring Boot RESTful web services, see the book [Lean Application Engineering Featuring Backbone.Marionette and the Spring Framework](https://leanpub.com/leanstacks-marionette-spring). 8 | 9 | LEAN**STACKS** offers several technology instruction video series, publications, and starter projects. For more information go to [LeanStacks.com](http://www.leanstacks.com/). 10 | 11 | ## Repository 12 | 13 | This repository is a companion for the LEAN**STACKS** YouTube channel playlist entitled [Spring Boot Fundamentals](https://www.youtube.com/playlist?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 14 | 15 | ### Repository Organization 16 | 17 | Each episode of the Spring Boot Fundamentals video series has a corresponding branch in this repository. For example, all of the source code illustrated in the episode entitled [Bootstrapping a Spring Boot Application Project](https://youtu.be/XbknBOmMuPQ?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY) may be found on the repository branch named [bootstrap](https://github.com/mwarman/spring-boot-fundamentals/tree/bootstrap). 18 | 19 | ### Branches 20 | 21 | #### bootstrap 22 | 23 | The branch named `bootstrap` contains the source code illustrated in the episode [Bootstrapping a Spring Boot Application Project](https://youtu.be/XbknBOmMuPQ?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 24 | 25 | #### restws-1 26 | 27 | The branch named `restws-1` contains the source code illustrated in the episode [Creating RESTful Web Services with Spring Boot - Part 1](https://youtu.be/kbisNUfqVLM?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 28 | 29 | #### restws-2 30 | 31 | The branch named `restws-2` contains the source code illustrated in the episode [Creating RESTful Web Services with Spring Boot - Part 2](https://youtu.be/mrrHTJxppi8?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 32 | 33 | #### service 34 | 35 | The branch named `service` contains the source code illustrated in the episode [Creating Service Components with Spring Boot](https://youtu.be/qJnAM_ZZvWA?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 36 | 37 | #### repository 38 | 39 | The branch named `repository` contains the source code illustrated in the episode [Using Spring Data Repositories with Spring Boot](https://youtu.be/4bPT-0f-am4?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 40 | 41 | #### transactional 42 | 43 | The branch named `transactional` contains the source code illustrated in the episode [Declarative Transaction Management with Spring Boot](https://youtu.be/4bPT-0f-am4?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 44 | 45 | #### cache 46 | 47 | The branch named `cache` contains the source code illustrated in the episode [Declarative Cache Management with Spring Boot](https://youtu.be/g4h268Hx0AU?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 48 | 49 | #### scheduled 50 | 51 | The branch named `scheduled` contains the source code illustrated in the episode [Creating Scheduled Processes with Spring Boot](https://youtu.be/TEMsEcdAsbY?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 52 | 53 | #### async 54 | 55 | The branch named `async` contains the source code illustrated in the episode [Creating Asynchronous Processes with Spring Boot](https://youtu.be/106WWFvgNW0?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 56 | 57 | #### configuration 58 | 59 | The branch named `configuration` contains the source code illustrated in the episode [Using Profiles and Properties to Create Environment-Specific Runtime Configurations with Spring Boot](https://youtu.be/0zjQX7WwjrI?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 60 | 61 | #### unit-test 62 | 63 | The branch named `unit-test` contains the source code illustrated in the episode [Creating Unit Tests with Spring Boot](https://youtu.be/WKD9E8KsQME?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 64 | 65 | #### unit-test-controller 66 | 67 | The branch named `unit-test-controller` contains the source code illustrated in the episode [Creating Web Service Controller Unit Tests with Spring Boot](https://youtu.be/zjVobP0sonA?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 68 | 69 | #### unit-test-mockito 70 | 71 | The branch named `unit-test-mockito` contains the source code illustrated in the episode [Creating Web Service Controller Unit Tests with Mockito and Spring Boot](https://youtu.be/7TMuBxTy3GE?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 72 | 73 | #### actuator 74 | 75 | The branch named `actuator` contains the source code illustrated in the episode [Production Monitoring and Management with Spring Boot Actuator](https://youtu.be/7L5rBQUMiPI?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 76 | 77 | #### security 78 | 79 | The branch named `security` contains the source code illustrated in the episode [Protecting Application Assets with Spring Security - Out-of-the-Box Features](https://youtu.be/CQfp2ngwT5U?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 80 | 81 | #### controller-hierarchy 82 | 83 | The branch named `controller-hierarchy` contains the source code illustrated in the episode [Creating Meaningful RESTful Web Service Controller Hierarchies with Spring Boot](https://youtu.be/Dg8hb0HPMWA?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 84 | 85 | #### gradle 86 | 87 | The branch named `gradle` contains the source code illustrated in the episode [Using the Gradle Build System](https://youtu.be/wRHj2hUHQd0?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 88 | 89 | #### gradle-build-dash 90 | 91 | The branch named `gradle-build-dash` contains the source code illustrated in the episode [Introduction to Gradle Project and Build Reports](https://youtu.be/I_jnSDjN2eo?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 92 | 93 | #### gradle-jacoco 94 | 95 | The branch named `gradle-jacoco` contains the source code illustrated in the episode [Using the Gradle JaCoCo Plugin for Unit Test Code Coverage Reporting](https://youtu.be/ieYs0hkogVY?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 96 | 97 | #### gradle-checkstyle 98 | 99 | The branch named `gradle-checkstyle` contains the source code illustrated in the episode [Using the Gradle Checkstyle Plugin for Code Style Reporting](https://youtu.be/zo3zyyo7Vkw?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 100 | 101 | #### gradle-pmd 102 | 103 | The branch named `gradle-pmd` contains the source code illustrated in the episode [Using the Gradle PMD Plugin for Static Code Analysis](https://youtu.be/Eek-5VJV2Xk?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 104 | 105 | #### gradle-defaulttasks 106 | 107 | The branch named `gradle-defaulttasks` contains the source code illustrated in the episode [Configuring Gradle Default Tasks](https://youtu.be/6NrFSDzwcfg?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 108 | 109 | #### upgrade-140 110 | 111 | The branch named `upgrade-140` contains the source code illustrated in the episode [Upgrading to Spring Boot 1.4](https://youtu.be/oxhhYZNKXAM?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 112 | 113 | #### executable 114 | 115 | The branch named `executable` contains the source code illustrated in the episode [Installing a Spring Boot Application on a Server - Part One](https://youtu.be/eCalW4OBpV8?list=PLGDwUiT1wr6-Fn3N2oqJpTdhGjFHnIIKY). 116 | 117 | #### resttemplate 118 | 119 | The branch named `resttemplate` contains the source code illustrated in the episode [Using RestTemplate and Spring Boot to Integrate with REST Services](). 120 | 121 | 122 | ## Languages 123 | 124 | This project is authored in Java. 125 | 126 | ## Installation 127 | 128 | ### Fork the Repository 129 | 130 | Fork the [Spring Boot Fundamentals](https://github.com/mwarman/spring-boot-fundamentals) repository on GitHub. Clone the project to your host machine. 131 | 132 | ### Dependencies 133 | 134 | The project requires the following dependencies be installed on the host machine: 135 | 136 | * Java Development Kit 7 or later 137 | 138 | and choose one of: 139 | * Apache Maven 3 or later 140 | * Gradle 2.12 or later 141 | 142 | ## Running 143 | 144 | The project supports [Maven](http://maven.apache.org/) and [Gradle](http://gradle.org/) for build, package, and test workflow automation. 145 | 146 | ### Maven 147 | 148 | The following Maven goals are the most commonly used. 149 | 150 | #### spring-boot:run 151 | 152 | The `spring-boot:run` Maven goal performs the following workflow steps: 153 | 154 | * compiles Java classes to the /target directory 155 | * copies all resources to the /target directory 156 | * starts an embedded Apache Tomcat server 157 | 158 | To execute the `spring-boot:run` Maven goal, type the following command at a terminal prompt in the project base directory. 159 | 160 | ``` 161 | mvn spring-boot:run 162 | ``` 163 | 164 | Type `ctrl-C` to halt the web server. 165 | 166 | This goal is used for local machine development and functional testing. Use the `package` goal for server deployment. 167 | 168 | #### test 169 | 170 | The `test` Maven goal performs the following workflow steps: 171 | 172 | * compiles Java classes to the /target directory 173 | * copies all resources to the /target directory 174 | * executes the unit test suites 175 | * produces unit test reports 176 | 177 | The `test` Maven goal is designed to allow engineers the means to run the unit test suites against the main source code. This goal may also be used on continuous integration servers such as Jenkins, etc. 178 | 179 | To execute the `test` Maven goal, type the following command at a terminal prompt in the project base directory. 180 | 181 | ``` 182 | mvn clean test 183 | ``` 184 | 185 | #### package 186 | 187 | The `package` Maven goal performs the following workflow steps: 188 | 189 | * compiles Java classes to the /target directory 190 | * copies all resources to the /target directory 191 | * executes the unit test suites 192 | * produces unit test reports 193 | * prepares an executable JAR file in the /target directory 194 | 195 | The `package` Maven goal is designed to prepare the application for distribution to server environments. The application and all dependencies are packaged into a single, executable JAR file. 196 | 197 | To execute the `package` goal, type the following command at a terminal prompt in the project base directory. 198 | 199 | ``` 200 | mvn clean package 201 | ``` 202 | 203 | The application distribution artifact is placed in the /target directory and is named using the `artifactId` and `version` from the pom.xml file. To run the JAR file use the following command: 204 | 205 | ``` 206 | java -jar example-1.0.0.jar 207 | ``` 208 | 209 | By default, the batch and hsqldb profiles are active. To run the application with a specific set of active profiles, supply the `--spring.profiles.active` command line argument. For example, to start the project using MySQL instad of HSQLDB and enable the batch process: 210 | 211 | ``` 212 | java -jar example-1.0.0.jar --spring.profiles.active=mysql,batch 213 | ``` 214 | 215 | ### Gradle 216 | 217 | The following Gradle tasks are the most commonly used. 218 | 219 | #### bootRun 220 | 221 | The `bootRun` Gradle task performs the following workflow steps: 222 | 223 | * compiles Java classes to the /build directory 224 | * copies all resources to the /build directory 225 | * starts an embedded Apache Tomcat server 226 | 227 | To execute the `bootRun` Gradle task, type the following command at a terminal prompt in the project base directory. 228 | 229 | ``` 230 | gradle bootRun 231 | ``` 232 | 233 | Type `ctrl-C` to halt the web server. 234 | 235 | This task is used for local machine development and functional testing. Use the `assemble` or `build` task for server deployment. 236 | 237 | #### assemble 238 | 239 | The `assemble` Gradle task performs the following workflow steps: 240 | 241 | * compiles Java classes to the /build directory 242 | * copies all resources to the /build directory 243 | * prepares an executable JAR file in the /build/libs directory 244 | 245 | The `assemble` Gradle task is designed to allow engineers the means to compile the project and produce an executable JAR file suitable for server environments without executing unit tests or producing other project reports. 246 | 247 | To execute the `assemble` Gradle task, type the following command at a terminal prompt in the project base directory. 248 | 249 | ``` 250 | gradle clean assemble 251 | ``` 252 | 253 | #### build 254 | 255 | The `build` Gradle task performs the following workflow steps: 256 | 257 | * compiles Java classes to the /build directory 258 | * copies all resources to the /build directory 259 | * executes the unit test suites 260 | * produces unit test reports 261 | * prepares an executable JAR file in the /build/libs directory 262 | 263 | The `build` Gradle task is prepares the application for distribution to server environments. The application and all dependencies are packaged into a single, executable JAR file. 264 | 265 | This task is ideal for use on continuous integration servers such as Jenkins, etc. because it produces unit test, code coverage, and static analysis reports. 266 | 267 | To execute the `build` Gradle task, type the following command at a terminal prompt in the project base directory. 268 | 269 | ``` 270 | gradle clean build 271 | ``` 272 | 273 | The application distribution artifact is placed in the /build/libs directory and is named using the project name and version from the `build.gradle` file. To run the JAR file use the following command: 274 | 275 | ``` 276 | java -jar build/libs/example-1.0.0.jar 277 | ``` 278 | 279 | By default, the batch and hsqldb profiles are active. To run the application with a specific set of active profiles, supply the `--spring.profiles.active` command line argument. For example, to start the project using MySQL instad of HSQLDB and enable the batch process: 280 | 281 | ``` 282 | java -jar build/libs/example-1.0.0.jar --spring.profiles.active=mysql,batch 283 | ``` 284 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '1.5.1.RELEASE' 4 | id 'eclipse' 5 | id 'jacoco' 6 | id 'checkstyle' 7 | id 'pmd' 8 | id 'project-report' 9 | id 'build-dashboard' 10 | } 11 | 12 | group = 'com.leanstacks' 13 | version = '1.0.0-SNAPSHOT' 14 | sourceCompatibility = 1.8 15 | 16 | ext { 17 | jacocoVersion = '0.7.7.201606060606' 18 | checkstyleVersion = '7.2' 19 | pmdVersion = '5.5.2' 20 | guavaVersion = '20.0' 21 | jadiraVersion = '5.0.0.GA' 22 | } 23 | 24 | repositories { 25 | jcenter() 26 | } 27 | 28 | dependencies { 29 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-web' 30 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-security' 31 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator' 32 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa' 33 | 34 | compile group: 'org.springframework', name: 'spring-context-support' 35 | 36 | compile group: 'joda-time', name: 'joda-time' 37 | compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-joda' 38 | compile group: 'com.google.guava', name: 'guava', version: guavaVersion 39 | compile group: 'com.github.ben-manes.caffeine', name: 'caffeine' 40 | compile group: 'org.jadira.usertype', name: 'usertype.extended', version: jadiraVersion 41 | 42 | runtime group: 'org.hsqldb', name: 'hsqldb' 43 | runtime group: 'mysql', name: 'mysql-connector-java' 44 | 45 | testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' 46 | } 47 | 48 | defaultTasks 'clean', 'build' 49 | 50 | springBoot { 51 | executable = true 52 | buildInfo() 53 | } 54 | 55 | jacoco { 56 | toolVersion = jacocoVersion 57 | } 58 | 59 | jacocoTestReport { 60 | reports { 61 | html.enabled = true 62 | xml.enabled = true 63 | csv.enabled = true 64 | } 65 | } 66 | test.finalizedBy jacocoTestReport 67 | 68 | checkstyle { 69 | toolVersion = checkstyleVersion 70 | config = rootProject.resources.text.fromFile('etc/checkstyle/rules.xml') 71 | } 72 | 73 | pmd { 74 | toolVersion = pmdVersion 75 | ruleSetConfig = rootProject.resources.text.fromFile('etc/pmd/ruleset.xml') 76 | ignoreFailures = true 77 | } 78 | 79 | check.finalizedBy projectReport 80 | 81 | projectReport.finalizedBy buildDashboard 82 | -------------------------------------------------------------------------------- /etc/checkstyle/rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 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 | 108 | 109 | 110 | 111 | 112 | 113 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 132 | 134 | 136 | 138 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 161 | 162 | 163 | 164 | 165 | 167 | 168 | 169 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 181 | 182 | 183 | 184 | 185 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /etc/eclipse/java-formatter.xml: -------------------------------------------------------------------------------- 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 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /etc/pmd/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | This is the LeanStacks Official PMD ruleset. 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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leanstacks/spring-boot-fundamentals/b98476c9ec9a10ac87757e0b76e1679a1f7f10bb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 15 07:04:38 EST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | 6 | org.example 7 | spring-boot-fundamentals 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 1.5.1.RELEASE 14 | 15 | 16 | 17 | UTF-8 18 | 1.8 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-security 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-jpa 38 | 39 | 40 | org.hsqldb 41 | hsqldb 42 | runtime 43 | 44 | 45 | 46 | 47 | org.springframework 48 | spring-context-support 49 | 50 | 51 | com.github.ben-manes.caffeine 52 | caffeine 53 | 54 | 55 | 56 | 57 | com.google.guava 58 | guava 59 | 20.0 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-actuator 66 | 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-test 72 | test 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-maven-plugin 81 | 82 | true 83 | 84 | 85 | 86 | 87 | build-info 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/Application.java: -------------------------------------------------------------------------------- 1 | package org.example.ws; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | /** 11 | * Spring Boot main application class. Serves as both the runtime application 12 | * entry point and the central Java configuration class. 13 | * 14 | * @author Matt Warman 15 | */ 16 | @SpringBootApplication 17 | @EnableTransactionManagement 18 | @EnableCaching 19 | @EnableScheduling 20 | @EnableAsync 21 | public class Application { 22 | 23 | /** 24 | * Entry point for the application. 25 | * 26 | * @param args Command line arguments. 27 | * @throws Exception Thrown when an unexpected Exception is thrown from the 28 | * application. 29 | */ 30 | public static void main(String[] args) throws Exception { 31 | SpringApplication.run(Application.class, args); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/actuator/health/GreetingHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.actuator.health; 2 | 3 | import java.util.Collection; 4 | 5 | import org.example.ws.model.Greeting; 6 | import org.example.ws.service.GreetingService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.actuate.health.Health; 9 | import org.springframework.boot.actuate.health.HealthIndicator; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * The GreetingHealthIndicator is a custom Spring Boot Actuator HealthIndicator 14 | * implementation. HealthIndicator classes are invoked when the Actuator 15 | * 'health' endpoint is invoked. Each HealthIndicator class assesses some 16 | * portion of the application's health, returing a Health object which indicates 17 | * that status and, optionally, additional health attributes. 18 | * 19 | * @author Matt Warman 20 | */ 21 | @Component 22 | public class GreetingHealthIndicator implements HealthIndicator { 23 | 24 | /** 25 | * The GreetingService business service. 26 | */ 27 | @Autowired 28 | private GreetingService greetingService; 29 | 30 | @Override 31 | public Health health() { 32 | 33 | // Assess the application's Greeting health. If the application's 34 | // Greeting components have data to service user requests, the Greeting 35 | // component is considered 'healthy', otherwise it is not. 36 | 37 | Collection greetings = greetingService.findAll(); 38 | 39 | if (greetings == null || greetings.size() == 0) { 40 | return Health.down().withDetail("count", 0).build(); 41 | } 42 | 43 | return Health.up().withDetail("count", greetings.size()).build(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/batch/GreetingBatchBean.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.batch; 2 | 3 | import java.util.Collection; 4 | 5 | import org.example.ws.model.Greeting; 6 | import org.example.ws.service.GreetingService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.scheduling.annotation.Scheduled; 12 | import org.springframework.stereotype.Component; 13 | 14 | /** 15 | * The GreetingBatchBean contains @Scheduled methods operating on 16 | * Greeting entities to perform batch operations. 17 | * 18 | * @author Matt Warman 19 | */ 20 | @Profile("batch") 21 | @Component 22 | public class GreetingBatchBean { 23 | 24 | /** 25 | * The Logger for this class. 26 | */ 27 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 28 | 29 | /** 30 | * The GreetingService business service. 31 | */ 32 | @Autowired 33 | private GreetingService greetingService; 34 | 35 | /** 36 | * Use a cron expression to execute logic on a schedule. 37 | * 38 | * Expression: second minute hour day-of-month month weekday 39 | * 40 | * @see http ://docs.spring.io/spring/docs/current/javadoc-api/org/ 41 | * springframework /scheduling/support/CronSequenceGenerator.html 42 | */ 43 | @Scheduled( 44 | cron = "${batch.greeting.cron}") 45 | public void cronJob() { 46 | logger.info("> cronJob"); 47 | 48 | // Add scheduled logic here 49 | Collection greetings = greetingService.findAll(); 50 | logger.info("There are {} greetings in the data store.", 51 | greetings.size()); 52 | 53 | logger.info("< cronJob"); 54 | } 55 | 56 | /** 57 | * Execute logic beginning at fixed intervals with a delay after the 58 | * application starts. Use the fixedRate element to indicate 59 | * how frequently the method is to be invoked. Use the 60 | * initialDelay element to indicate how long to wait after 61 | * application startup to schedule the first execution. 62 | */ 63 | @Scheduled( 64 | initialDelayString = "${batch.greeting.initialdelay}", 65 | fixedRateString = "${batch.greeting.fixedrate}") 66 | public void fixedRateJobWithInitialDelay() { 67 | logger.info("> fixedRateJobWithInitialDelay"); 68 | 69 | // Add scheduled logic here 70 | 71 | // Simulate job processing time 72 | long pause = 5000; 73 | long start = System.currentTimeMillis(); 74 | do { 75 | if (start + pause < System.currentTimeMillis()) { 76 | break; 77 | } 78 | } while (true); 79 | logger.info("Processing time was {} seconds.", pause / 1000); 80 | 81 | logger.info("< fixedRateJobWithInitialDelay"); 82 | } 83 | 84 | /** 85 | * Execute logic with a delay between the end of the last execution and the 86 | * beginning of the next. Use the fixedDelay element to 87 | * indicate the time to wait between executions. Use the 88 | * initialDelay element to indicate how long to wait after 89 | * application startup to schedule the first execution. 90 | */ 91 | @Scheduled( 92 | initialDelayString = "${batch.greeting.initialdelay}", 93 | fixedDelayString = "${batch.greeting.fixeddelay}") 94 | public void fixedDelayJobWithInitialDelay() { 95 | logger.info("> fixedDelayJobWithInitialDelay"); 96 | 97 | // Add scheduled logic here 98 | 99 | // Simulate job processing time 100 | long pause = 5000; 101 | long start = System.currentTimeMillis(); 102 | do { 103 | if (start + pause < System.currentTimeMillis()) { 104 | break; 105 | } 106 | } while (true); 107 | logger.info("Processing time was {} seconds.", pause / 1000); 108 | 109 | logger.info("< fixedDelayJobWithInitialDelay"); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/model/Greeting.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.Id; 6 | 7 | @Entity 8 | public class Greeting { 9 | 10 | @Id 11 | @GeneratedValue 12 | private Long id; 13 | 14 | private String text; 15 | 16 | public Greeting() { 17 | 18 | } 19 | 20 | public Long getId() { 21 | return id; 22 | } 23 | 24 | public void setId(Long id) { 25 | this.id = id; 26 | } 27 | 28 | public String getText() { 29 | return text; 30 | } 31 | 32 | public void setText(String text) { 33 | this.text = text; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/repository/GreetingRepository.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.repository; 2 | 3 | import org.example.ws.model.Greeting; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface GreetingRepository extends JpaRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | import org.example.ws.model.Greeting; 6 | 7 | /** 8 | * The EmailService interface defines all public business behaviors for 9 | * composing and transmitting email messages. 10 | * 11 | * This interface should be injected into EmailService clients, not the 12 | * implementation bean. 13 | * 14 | * @author Matt Warman 15 | */ 16 | public interface EmailService { 17 | 18 | /** 19 | * Send a Greeting via email synchronously. 20 | * @param greeting A Greeting to send. 21 | * @return A Boolean whose value is TRUE if sent successfully; otherwise 22 | * FALSE. 23 | */ 24 | Boolean send(Greeting greeting); 25 | 26 | /** 27 | * Send a Greeting via email asynchronously. 28 | * @param greeting A Greeting to send. 29 | */ 30 | void sendAsync(Greeting greeting); 31 | 32 | /** 33 | * Send a Greeting via email asynchronously. Returns a Future<Boolean> 34 | * response allowing the client to obtain the status of the operation once 35 | * it is completed. 36 | * @param greeting A Greeting to send. 37 | * @return A Future<Boolean> whose value is TRUE if sent successfully; 38 | * otherwise, FALSE. 39 | */ 40 | Future sendAsyncWithResult(Greeting greeting); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/EmailServiceBean.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | import org.example.ws.model.Greeting; 6 | import org.example.ws.util.AsyncResponse; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.scheduling.annotation.Async; 10 | import org.springframework.stereotype.Service; 11 | 12 | /** 13 | * The EmailServiceBean implements all business behaviors defined by the 14 | * EmailService interface. 15 | * 16 | * @author Matt Warman 17 | */ 18 | @Service 19 | public class EmailServiceBean implements EmailService { 20 | 21 | /** 22 | * The Logger for this class. 23 | */ 24 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 25 | 26 | @Override 27 | public Boolean send(Greeting greeting) { 28 | logger.info("> send"); 29 | 30 | Boolean success = Boolean.FALSE; 31 | 32 | // Simulate method execution time 33 | long pause = 5000; 34 | try { 35 | Thread.sleep(pause); 36 | } catch (Exception e) { 37 | // do nothing 38 | } 39 | logger.info("Processing time was {} seconds.", pause / 1000); 40 | 41 | success = Boolean.TRUE; 42 | 43 | logger.info("< send"); 44 | return success; 45 | } 46 | 47 | @Async 48 | @Override 49 | public void sendAsync(Greeting greeting) { 50 | logger.info("> sendAsync"); 51 | 52 | try { 53 | send(greeting); 54 | } catch (Exception e) { 55 | logger.warn("Exception caught sending asynchronous mail.", e); 56 | } 57 | 58 | logger.info("< sendAsync"); 59 | } 60 | 61 | @Async 62 | @Override 63 | public Future sendAsyncWithResult(Greeting greeting) { 64 | logger.info("> sendAsyncWithResult"); 65 | 66 | AsyncResponse response = new AsyncResponse(); 67 | 68 | try { 69 | Boolean success = send(greeting); 70 | response.complete(success); 71 | } catch (Exception e) { 72 | logger.warn("Exception caught sending asynchronous mail.", e); 73 | response.completeExceptionally(e); 74 | } 75 | 76 | logger.info("< sendAsyncWithResult"); 77 | return response; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/GreetingService.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import java.util.Collection; 4 | 5 | import org.example.ws.model.Greeting; 6 | 7 | /** 8 | * The GreetingService interface defines all public business behaviors for 9 | * operations on the Greeting entity model. 10 | * 11 | * This interface should be injected into GreetingService clients, not the 12 | * implementation bean. 13 | * 14 | * @author Matt Warman 15 | */ 16 | public interface GreetingService { 17 | 18 | /** 19 | * Find all Greeting entities. 20 | * @return A Collection of Greeting objects. 21 | */ 22 | Collection findAll(); 23 | 24 | /** 25 | * Find a single Greeting entity by primary key identifier. 26 | * @param id A Long primary key identifier. 27 | * @return A Greeting or null if none found. 28 | */ 29 | Greeting findOne(Long id); 30 | 31 | /** 32 | * Persists a Greeting entity in the data store. 33 | * @param greeting A Greeting object to be persisted. 34 | * @return The persisted Greeting entity. 35 | */ 36 | Greeting create(Greeting greeting); 37 | 38 | /** 39 | * Updates a previously persisted Greeting entity in the data store. 40 | * @param greeting A Greeting object to be updated. 41 | * @return The updated Greeting entity. 42 | */ 43 | Greeting update(Greeting greeting); 44 | 45 | /** 46 | * Removes a previously persisted Greeting entity from the data store. 47 | * @param id A Long primary key identifier. 48 | */ 49 | void delete(Long id); 50 | 51 | /** 52 | * Evicts all members of the "greetings" cache. 53 | */ 54 | void evictCache(); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/GreetingServiceBean.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.persistence.EntityExistsException; 6 | import javax.persistence.NoResultException; 7 | 8 | import org.example.ws.model.Greeting; 9 | import org.example.ws.repository.GreetingRepository; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.actuate.metrics.CounterService; 14 | import org.springframework.cache.annotation.CacheEvict; 15 | import org.springframework.cache.annotation.CachePut; 16 | import org.springframework.cache.annotation.Cacheable; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.transaction.annotation.Propagation; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | /** 22 | * The GreetingServiceBean encapsulates all business behaviors operating on the 23 | * Greeting entity model object. 24 | * 25 | * @author Matt Warman 26 | */ 27 | @Service 28 | @Transactional( 29 | propagation = Propagation.SUPPORTS, 30 | readOnly = true) 31 | public class GreetingServiceBean implements GreetingService { 32 | 33 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 34 | 35 | /** 36 | * The CounterService captures metrics for Spring Actuator. 37 | */ 38 | @Autowired 39 | private CounterService counterService; 40 | 41 | /** 42 | * The Spring Data repository for Greeting entities. 43 | */ 44 | @Autowired 45 | private GreetingRepository greetingRepository; 46 | 47 | @Override 48 | public Collection findAll() { 49 | logger.info("> findAll"); 50 | 51 | counterService.increment("method.invoked.greetingServiceBean.findAll"); 52 | 53 | Collection greetings = greetingRepository.findAll(); 54 | 55 | logger.info("< findAll"); 56 | return greetings; 57 | } 58 | 59 | @Override 60 | @Cacheable( 61 | value = "greetings", 62 | key = "#id") 63 | public Greeting findOne(Long id) { 64 | logger.info("> findOne id:{}", id); 65 | 66 | counterService.increment("method.invoked.greetingServiceBean.findOne"); 67 | 68 | Greeting greeting = greetingRepository.findOne(id); 69 | 70 | logger.info("< findOne id:{}", id); 71 | return greeting; 72 | } 73 | 74 | @Override 75 | @Transactional( 76 | propagation = Propagation.REQUIRED, 77 | readOnly = false) 78 | @CachePut( 79 | value = "greetings", 80 | key = "#result.id") 81 | public Greeting create(Greeting greeting) { 82 | logger.info("> create"); 83 | 84 | counterService.increment("method.invoked.greetingServiceBean.create"); 85 | 86 | // Ensure the entity object to be created does NOT exist in the 87 | // repository. Prevent the default behavior of save() which will update 88 | // an existing entity if the entity matching the supplied id exists. 89 | if (greeting.getId() != null) { 90 | // Cannot create Greeting with specified ID value 91 | logger.error( 92 | "Attempted to create a Greeting, but id attribute was not null."); 93 | throw new EntityExistsException( 94 | "The id attribute must be null to persist a new entity."); 95 | } 96 | 97 | Greeting savedGreeting = greetingRepository.save(greeting); 98 | 99 | logger.info("< create"); 100 | return savedGreeting; 101 | } 102 | 103 | @Override 104 | @Transactional( 105 | propagation = Propagation.REQUIRED, 106 | readOnly = false) 107 | @CachePut( 108 | value = "greetings", 109 | key = "#greeting.id") 110 | public Greeting update(Greeting greeting) { 111 | logger.info("> update id:{}", greeting.getId()); 112 | 113 | counterService.increment("method.invoked.greetingServiceBean.update"); 114 | 115 | // Ensure the entity object to be updated exists in the repository to 116 | // prevent the default behavior of save() which will persist a new 117 | // entity if the entity matching the id does not exist 118 | Greeting greetingToUpdate = findOne(greeting.getId()); 119 | if (greetingToUpdate == null) { 120 | // Cannot update Greeting that hasn't been persisted 121 | logger.error( 122 | "Attempted to update a Greeting, but the entity does not exist."); 123 | throw new NoResultException("Requested entity not found."); 124 | } 125 | 126 | greetingToUpdate.setText(greeting.getText()); 127 | Greeting updatedGreeting = greetingRepository.save(greetingToUpdate); 128 | 129 | logger.info("< update id:{}", greeting.getId()); 130 | return updatedGreeting; 131 | } 132 | 133 | @Override 134 | @Transactional( 135 | propagation = Propagation.REQUIRED, 136 | readOnly = false) 137 | @CacheEvict( 138 | value = "greetings", 139 | key = "#id") 140 | public void delete(Long id) { 141 | logger.info("> delete id:{}", id); 142 | 143 | counterService.increment("method.invoked.greetingServiceBean.delete"); 144 | 145 | greetingRepository.delete(id); 146 | 147 | logger.info("< delete id:{}", id); 148 | } 149 | 150 | @Override 151 | @CacheEvict( 152 | value = "greetings", 153 | allEntries = true) 154 | public void evictCache() { 155 | logger.info("> evictCache"); 156 | logger.info("< evictCache"); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/Quote.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 7 | 8 | /** 9 | * The Quote class models the attributes of a Quote, a famous or meaningful 10 | * phrase. 11 | * 12 | * @author Matt Warman 13 | */ 14 | @JsonIgnoreProperties( 15 | ignoreUnknown = true) 16 | public class Quote { 17 | 18 | /** 19 | * A uniquie identifier. 20 | */ 21 | private String id; 22 | /** 23 | * The quote, or quote category, title. 24 | */ 25 | private String title; 26 | /** 27 | * The quote text. 28 | */ 29 | private String quote; 30 | /** 31 | * The length of the quote text. 32 | */ 33 | private String length; 34 | /** 35 | * The date the quote was provided by the external service. 36 | */ 37 | private Date date; 38 | /** 39 | * The person or entity to whom the quote is attributed. 40 | */ 41 | private String author; 42 | /** 43 | * An image associated with the quote text. 44 | */ 45 | private String background; 46 | /** 47 | * The category or general grouping to which the quote text belongs. 48 | */ 49 | private String category; 50 | /** 51 | * An array of tags, often the categories, which are applicable to the quote 52 | * text. 53 | */ 54 | private ArrayList tags; 55 | 56 | /** 57 | * Construct a Quote instance. 58 | */ 59 | public Quote() { 60 | 61 | } 62 | 63 | public String getId() { 64 | return id; 65 | } 66 | 67 | public void setId(String id) { 68 | this.id = id; 69 | } 70 | 71 | public String getTitle() { 72 | return title; 73 | } 74 | 75 | public void setTitle(String title) { 76 | this.title = title; 77 | } 78 | 79 | public String getQuote() { 80 | return quote; 81 | } 82 | 83 | public void setQuote(String quote) { 84 | this.quote = quote; 85 | } 86 | 87 | public String getLength() { 88 | return length; 89 | } 90 | 91 | public void setLength(String length) { 92 | this.length = length; 93 | } 94 | 95 | public Date getDate() { 96 | return date; 97 | } 98 | 99 | public void setDate(Date date) { 100 | this.date = date; 101 | } 102 | 103 | public String getAuthor() { 104 | return author; 105 | } 106 | 107 | public void setAuthor(String author) { 108 | this.author = author; 109 | } 110 | 111 | public String getBackground() { 112 | return background; 113 | } 114 | 115 | public void setBackground(String background) { 116 | this.background = background; 117 | } 118 | 119 | public String getCategory() { 120 | return category; 121 | } 122 | 123 | public void setCategory(String category) { 124 | this.category = category; 125 | } 126 | 127 | public ArrayList getTags() { 128 | return tags; 129 | } 130 | 131 | public void setTags(ArrayList tags) { 132 | this.tags = tags; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/QuoteResponse.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | /** 6 | * The QuoteResponse class models the attributes of a response from the "They 7 | * Said So" (TSS) remote Quote API. This class represents the top-level model of 8 | * the API response. 9 | * 10 | * @author Matt Warman 11 | */ 12 | @JsonIgnoreProperties( 13 | ignoreUnknown = true) 14 | public class QuoteResponse { 15 | 16 | /** 17 | * The success or failure status of the API call. 18 | */ 19 | private QuoteResponseSuccess success; 20 | /** 21 | * The body of the API call response. 22 | */ 23 | private QuoteResponseContents contents; 24 | 25 | /** 26 | * Construct a new QuoteResponse. 27 | */ 28 | public QuoteResponse() { 29 | 30 | } 31 | 32 | /** 33 | * Returns the QuoteResponseSuccess object. 34 | * @return A QuoteResponseSuccess object. 35 | */ 36 | public QuoteResponseSuccess getSuccess() { 37 | return success; 38 | } 39 | 40 | /** 41 | * Sets the QuoteResponseSuccess object. 42 | * @param success A QuoteResponseSuccess object. 43 | */ 44 | public void setSuccess(QuoteResponseSuccess success) { 45 | this.success = success; 46 | } 47 | 48 | /** 49 | * A helper method which examines the internal value of the 50 | * QuoteResponseSuccess object and returns a boolean indicating the success 51 | * or failure of the API call. 52 | * @return A boolean whose value is true if the API call was 53 | * successful, otherwise returns false. 54 | */ 55 | public boolean isSuccess() { 56 | if (success != null && success.getTotal() > 0) { 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | /** 63 | * Returns the QuoteResponseContents object. 64 | * @return A QuoteResponseContents object. 65 | */ 66 | public QuoteResponseContents getContents() { 67 | return contents; 68 | } 69 | 70 | /** 71 | * Sets the QuoteResponseContents object. 72 | * @param contents A QuoteResponseContents object. 73 | */ 74 | public void setContents(QuoteResponseContents contents) { 75 | this.contents = contents; 76 | } 77 | 78 | /** 79 | * A helper method which examines the internal value of the 80 | * QuoteResponseContents object and returns the first Quote object from the 81 | * Collection if the API call was successful and the Collection is not 82 | * empty. 83 | * @return A Quote object or null. 84 | */ 85 | public Quote getQuote() { 86 | if (isSuccess()) { 87 | if (contents != null && contents.getQuotes().size() > 0) { 88 | return contents.getQuotes().get(0); 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/QuoteResponseContents.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | import java.util.ArrayList; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | 7 | /** 8 | * The QuoteResponseContents class is a container for the body of a response 9 | * from the "They Said So" (TSS) remote Quote API. This class holds the 10 | * object(s) returned from the call. 11 | * 12 | * @author Matt Warman 13 | */ 14 | @JsonIgnoreProperties( 15 | ignoreUnknown = true) 16 | public class QuoteResponseContents { 17 | 18 | /** 19 | * An array of Quote objects. 20 | */ 21 | private ArrayList quotes = new ArrayList(0); 22 | 23 | /** 24 | * Contructs a new QuoteResponseContents object. 25 | */ 26 | public QuoteResponseContents() { 27 | 28 | } 29 | 30 | /** 31 | * Returns the array of Quote objects. 32 | * @return An array of Quote objects. 33 | */ 34 | public ArrayList getQuotes() { 35 | return quotes; 36 | } 37 | 38 | /** 39 | * Sets the array of Quote objects. 40 | * @param quotes An array of Quote objects. 41 | */ 42 | public void setQuotes(ArrayList quotes) { 43 | if (quotes == null) { 44 | this.quotes = new ArrayList(0); 45 | } 46 | this.quotes = quotes; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/QuoteResponseSuccess.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | /** 6 | * The QuoteResponseSuccess class models the invocation status attributes of a 7 | * response from the "They Said So" (TSS) remote Quote API. This class 8 | * represents the pass-fail indicator model of the API response. 9 | * 10 | * @author Matt Warman 11 | */ 12 | @JsonIgnoreProperties( 13 | ignoreUnknown = true) 14 | public class QuoteResponseSuccess { 15 | 16 | /** 17 | * The total number of objects contained in the response. 18 | */ 19 | private int total; 20 | 21 | /** 22 | * Contruct a new QuoteResponseSuccess object. 23 | */ 24 | public QuoteResponseSuccess() { 25 | 26 | } 27 | 28 | /** 29 | * Returns the value of the total attribute. 30 | * @return An int value. 31 | */ 32 | public int getTotal() { 33 | return total; 34 | } 35 | 36 | /** 37 | * Sets the value of the total attribute. 38 | * @param total An int value. 39 | */ 40 | public void setTotal(int total) { 41 | this.total = total; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/QuoteService.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | /** 4 | * The QuoteService interface defines all public business behaviors for 5 | * operations Quote objects. 6 | * 7 | * This interface should be injected into QuoteService clients, not the 8 | * implementation bean. 9 | * 10 | * @author Matt Warman 11 | */ 12 | public interface QuoteService { 13 | 14 | /** 15 | * The 'inspirational' Quote category value. 16 | */ 17 | String CATEGORY_INSPIRATIONAL = "inspire"; 18 | 19 | /** 20 | * Retrieves the Quote of the day. A daily rotating Quote object. 21 | * @param category An optional String value of a Quote category to retrieve. 22 | * If not specified, the default category value is used. 23 | * @return A Quote object or null if none found. 24 | */ 25 | Quote getDaily(String category); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/quote/tss/QuoteServiceBean.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service.quote.tss; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.actuate.metrics.CounterService; 7 | import org.springframework.boot.web.client.RestTemplateBuilder; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | /** 12 | * The QuoteServiceBean encapsulates all business behaviors operating on the 13 | * Quote class. 14 | * 15 | * @author Matt Warman 16 | */ 17 | @Service 18 | public class QuoteServiceBean implements QuoteService { 19 | 20 | /** 21 | * The Logger for this class. 22 | */ 23 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 24 | 25 | /** 26 | * The CounterService captures metrics for Spring Actuator. 27 | */ 28 | @Autowired 29 | private CounterService counterService; 30 | 31 | /** 32 | * The RestTemplate used to retrieve data from the remote Quote API. 33 | */ 34 | private final RestTemplate restTemplate; 35 | 36 | /** 37 | * Construct a QuoteServiceBean with a RestTemplateBuilder used to 38 | * instantiate the RestTemplate used by this business service. 39 | * @param restTemplateBuilder A RestTemplateBuilder injected from the 40 | * ApplicationContext. 41 | */ 42 | public QuoteServiceBean(RestTemplateBuilder restTemplateBuilder) { 43 | this.restTemplate = restTemplateBuilder.build(); 44 | } 45 | 46 | @Override 47 | public Quote getDaily(String category) { 48 | logger.info("> getDaily"); 49 | 50 | counterService.increment("method.invoked.quoteServiceBean.getDaily"); 51 | 52 | String quoteCategory = QuoteService.CATEGORY_INSPIRATIONAL; 53 | if (category != null && category.trim().length() > 0) { 54 | quoteCategory = category.trim(); 55 | } 56 | 57 | QuoteResponse quoteResponse = this.restTemplate.getForObject( 58 | "http://quotes.rest/qod.json?category={cat}", 59 | QuoteResponse.class, quoteCategory); 60 | 61 | Quote quote = quoteResponse.getQuote(); 62 | 63 | logger.info("< getDaily"); 64 | return quote; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/util/AsyncResponse.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.util; 2 | 3 | import java.util.concurrent.CancellationException; 4 | import java.util.concurrent.ExecutionException; 5 | import java.util.concurrent.Future; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.concurrent.TimeoutException; 8 | 9 | /** 10 | * The AsyncResponse class implements the Future interface. This class 11 | * facilitates the normal and exceptional completion of asynchronous tasks (or 12 | * methods) and wraps their response. 13 | * 14 | * The AsyncResponse class seeks to mimic some behaviors defined in the 15 | * CompletableFuture class provided in JDK version 8. If using JDK 7 or earlier, 16 | * the AsyncResponse class is a suitable substitute for CompletableFuture. 17 | * 18 | * @author Matt Warman 19 | * 20 | * @param The type of Value object wrapped and returned by the 21 | * AsyncResponse. 22 | */ 23 | public class AsyncResponse implements Future { 24 | 25 | /** 26 | * Indicates the block operation should run indefinitely until the 27 | * AsyncResponse state changes. 28 | */ 29 | private static final long BLOCK_INDEFINITELY = 0; 30 | 31 | /** 32 | * The value returned from the task. 33 | */ 34 | private V value; 35 | /** 36 | * The exception, if any, thrown by the task. 37 | */ 38 | private Exception executionException; 39 | /** 40 | * TRUE if the task throws an Exception. Otherwise FALSE. 41 | */ 42 | private boolean isCompletedExceptionally = false; 43 | /** 44 | * TRUE when the task is cancelled or interrupted. Otherwise FALSE. 45 | */ 46 | private boolean isCancelled = false; 47 | /** 48 | * TRUE when the task is complete. Otherwise FALSE. 49 | */ 50 | private boolean isDone = false; 51 | /** 52 | * The interval, in milliseconds, which any get method checks 53 | * if the task is complete. Default: 100 milliseconds. 54 | */ 55 | private long checkCompletedInterval = 100; 56 | 57 | /** 58 | * Create a new AsyncResponse which has no value and is not complete. 59 | */ 60 | public AsyncResponse() { 61 | 62 | } 63 | 64 | /** 65 | * Create a new, completed AsyncResponse with the supplied value. 66 | * @param val An object of type V used as the task response value. 67 | */ 68 | public AsyncResponse(V val) { 69 | this.value = val; 70 | this.isDone = true; 71 | } 72 | 73 | /** 74 | * Create a new, completed AsyncResponse with the supplied Exception. The 75 | * AsyncResponse is marked as completed exceptionally. When the client 76 | * invokes one of the get methods, an ExecutionException will 77 | * be thrown using the supplied Exception as the cause of the 78 | * ExecutionException. 79 | * 80 | * @param ex A Throwable. 81 | */ 82 | public AsyncResponse(Throwable ex) { 83 | this.executionException = new ExecutionException(ex); 84 | this.isCompletedExceptionally = true; 85 | this.isDone = true; 86 | } 87 | 88 | @Override 89 | public boolean cancel(boolean mayInterruptIfRunning) { 90 | this.isCancelled = true; 91 | this.isDone = true; 92 | 93 | return false; 94 | } 95 | 96 | @Override 97 | public boolean isCancelled() { 98 | return this.isCancelled; 99 | } 100 | 101 | public boolean isCompletedExceptionally() { 102 | return this.isCompletedExceptionally; 103 | } 104 | 105 | @Override 106 | public boolean isDone() { 107 | return this.isDone; 108 | } 109 | 110 | @Override 111 | public V get() throws InterruptedException, ExecutionException { 112 | 113 | block(BLOCK_INDEFINITELY); 114 | 115 | if (isCancelled()) { 116 | throw new CancellationException(); 117 | } 118 | if (isCompletedExceptionally()) { 119 | throw new ExecutionException(this.executionException); 120 | } 121 | if (isDone()) { 122 | return this.value; 123 | } 124 | 125 | throw new InterruptedException(); 126 | } 127 | 128 | @Override 129 | public V get(long timeout, TimeUnit unit) 130 | throws InterruptedException, ExecutionException, TimeoutException { 131 | 132 | long timeoutInMillis = unit.toMillis(timeout); 133 | block(timeoutInMillis); 134 | 135 | if (isCancelled()) { 136 | throw new CancellationException(); 137 | } 138 | if (isCompletedExceptionally()) { 139 | throw new ExecutionException(this.executionException); 140 | } 141 | if (isDone()) { 142 | return this.value; 143 | } 144 | 145 | throw new InterruptedException(); 146 | } 147 | 148 | /** 149 | * Mark this AsyncResponse as finished (completed) and set the supplied 150 | * value V as the task return value. 151 | * @param val An object of type V. 152 | * @return A boolean that when TRUE indicates the AsyncResponse state was 153 | * successfully updated. A response of FALSE indicates the 154 | * AsyncResponse state could not be set correctly. 155 | */ 156 | public boolean complete(V val) { 157 | this.value = val; 158 | this.isDone = true; 159 | 160 | return true; 161 | } 162 | 163 | /** 164 | * Mark this AsyncResposne as finished (completed) with an exception. The 165 | * AsyncResponse value (V) is set to null. The supplied Throwable will be 166 | * used as the Cause of an ExceptionException thrown when any 167 | * get method is called. 168 | * 169 | * @param ex A Throwable. 170 | * @return A boolean that when TRUE indicates the AsyncResponse state was 171 | * successfully updated. A response of FALSE indicates the 172 | * AsyncResponse state could not be set correctly. 173 | */ 174 | public boolean completeExceptionally(Throwable ex) { 175 | this.value = null; 176 | this.executionException = new ExecutionException(ex); 177 | this.isCompletedExceptionally = true; 178 | this.isDone = true; 179 | 180 | return true; 181 | } 182 | 183 | /** 184 | * Set the interval at which any get method evaluates if the 185 | * AsyncResponse is complete or cancelled. 186 | * @param millis A long number of milliseconds. 187 | */ 188 | public void setCheckCompletedInterval(long millis) { 189 | this.checkCompletedInterval = millis; 190 | } 191 | 192 | /** 193 | * Pauses the current thread until the AsyncResponse is in a completed or 194 | * cancelled status OR the specified timeout (in milliseconds) has elapsed. 195 | * If the timeout value is zero (0), then wait indefinitely for the 196 | * AsyncResponse to be completed or cancelled. 197 | * 198 | * @param timeout A long number of milliseconds after which the process 199 | * ceases to wait for state change. 200 | * @throws InterruptedException Thrown when the blocking operation is 201 | * interrupted. 202 | */ 203 | private void block(long timeout) throws InterruptedException { 204 | long start = System.currentTimeMillis(); 205 | 206 | // Block until done, cancelled, or the timeout is exceeded 207 | while (!isDone() && !isCancelled()) { 208 | if (timeout > BLOCK_INDEFINITELY) { 209 | long now = System.currentTimeMillis(); 210 | if (now > start + timeout) { 211 | break; 212 | } 213 | } 214 | Thread.sleep(checkCompletedInterval); 215 | } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/DefaultExceptionAttributes.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web; 2 | 3 | import java.util.Date; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | 9 | import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.context.request.RequestAttributes; 12 | 13 | /** 14 | * The default implementation of {@link ExceptionAttributes}. This 15 | * implementation seeks to be similar to the {@link DefaultErrorAttributes} 16 | * class, but differs in the source of the attribute data. The 17 | * DefaultErrorAttributes class requires the exception to be thrown from the 18 | * Controller so that it may gather attribute values from 19 | * {@link RequestAttributes}. This class uses the {@link Exception}, 20 | * {@link HttpServletRequest}, and {@link HttpStatus} values. 21 | * 22 | * Provides a Map of the following attributes when they are available: 23 | *
    24 | *
  • timestamp - The time that the exception attributes were processed 25 | *
  • status - The HTTP status code in the response 26 | *
  • error - The HTTP status reason text 27 | *
  • exception - The class name of the Exception 28 | *
  • message - The Exception message 29 | *
  • path - The HTTP request servlet path when the exception was thrown 30 | *
31 | * 32 | * @author Matt Warman 33 | * @see ExceptionAttributes 34 | * 35 | */ 36 | public class DefaultExceptionAttributes implements ExceptionAttributes { 37 | 38 | /** 39 | * The timestamp attribute key. 40 | */ 41 | public static final String TIMESTAMP = "timestamp"; 42 | /** 43 | * The status attribute key. 44 | */ 45 | public static final String STATUS = "status"; 46 | /** 47 | * The error attribute key. 48 | */ 49 | public static final String ERROR = "error"; 50 | /** 51 | * The exception attribute key. 52 | */ 53 | public static final String EXCEPTION = "exception"; 54 | /** 55 | * The message attribute key. 56 | */ 57 | public static final String MESSAGE = "message"; 58 | /** 59 | * The path attribute key. 60 | */ 61 | public static final String PATH = "path"; 62 | 63 | @Override 64 | public Map getExceptionAttributes(Exception exception, 65 | HttpServletRequest httpRequest, HttpStatus httpStatus) { 66 | 67 | Map exceptionAttributes = new LinkedHashMap(); 68 | 69 | exceptionAttributes.put(TIMESTAMP, new Date()); 70 | addHttpStatus(exceptionAttributes, httpStatus); 71 | addExceptionDetail(exceptionAttributes, exception); 72 | addPath(exceptionAttributes, httpRequest); 73 | 74 | return exceptionAttributes; 75 | } 76 | 77 | /** 78 | * Adds the status and error attribute values from the {@link HttpStatus} 79 | * value. 80 | * @param exceptionAttributes The Map of exception attributes. 81 | * @param httpStatus The HttpStatus enum value. 82 | */ 83 | private void addHttpStatus(Map exceptionAttributes, 84 | HttpStatus httpStatus) { 85 | exceptionAttributes.put(STATUS, httpStatus.value()); 86 | exceptionAttributes.put(ERROR, httpStatus.getReasonPhrase()); 87 | } 88 | 89 | /** 90 | * Adds the exception and message attribute values from the 91 | * {@link Exception}. 92 | * @param exceptionAttributes The Map of exception attributes. 93 | * @param exception The Exception object. 94 | */ 95 | private void addExceptionDetail(Map exceptionAttributes, 96 | Exception exception) { 97 | exceptionAttributes.put(EXCEPTION, exception.getClass().getName()); 98 | exceptionAttributes.put(MESSAGE, exception.getMessage()); 99 | } 100 | 101 | /** 102 | * Adds the path attribute value from the {@link HttpServletRequest}. 103 | * @param exceptionAttributes The Map of exception attributes. 104 | * @param httpRequest The HttpServletRequest object. 105 | */ 106 | private void addPath(Map exceptionAttributes, 107 | HttpServletRequest httpRequest) { 108 | exceptionAttributes.put(PATH, httpRequest.getServletPath()); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/ExceptionAttributes.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web; 2 | 3 | import java.util.Map; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | 11 | /** 12 | * The ExceptionAttributes interface defines the behavioral contract to be 13 | * implemented by concrete ExceptionAttributes classes. 14 | * 15 | * Provides attributes which describe Exceptions and the context in which they 16 | * occurred. 17 | * 18 | * @author Matt Warman 19 | * @see DefaultExceptionAttributes 20 | * 21 | */ 22 | public interface ExceptionAttributes { 23 | 24 | /** 25 | * Returns a {@link Map} of exception attributes. The Map may be used to 26 | * display an error page or serialized into a {@link ResponseBody}. 27 | * 28 | * @param exception The Exception reported. 29 | * @param httpRequest The HttpServletRequest in which the Exception 30 | * occurred. 31 | * @param httpStatus The HttpStatus value that will be used in the 32 | * {@link HttpServletResponse}. 33 | * @return A Map of exception attributes. 34 | */ 35 | Map getExceptionAttributes(Exception exception, 36 | HttpServletRequest httpRequest, HttpStatus httpStatus); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/api/BaseController.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import java.util.Map; 4 | 5 | import javax.persistence.NoResultException; 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | import org.example.ws.web.DefaultExceptionAttributes; 9 | import org.example.ws.web.ExceptionAttributes; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | 16 | /** 17 | * The BaseController class implements common functionality for all Controller 18 | * classes. The @ExceptionHandler methods provide a consistent 19 | * response when Exceptions are thrown from @RequestMapping 20 | * annotated Controller methods. 21 | * 22 | * @author Matt Warman 23 | */ 24 | public class BaseController { 25 | 26 | /** 27 | * The Logger for this class. 28 | */ 29 | protected Logger logger = LoggerFactory.getLogger(this.getClass()); 30 | 31 | /** 32 | * Handles JPA NoResultExceptions thrown from web service controller 33 | * methods. Creates a response with Exception Attributes as JSON and HTTP 34 | * status code 404, not found. 35 | * 36 | * @param noResultException A NoResultException instance. 37 | * @param request The HttpServletRequest in which the NoResultException was 38 | * raised. 39 | * @return A ResponseEntity containing the Exception Attributes in the body 40 | * and HTTP status code 404. 41 | */ 42 | @ExceptionHandler(NoResultException.class) 43 | public ResponseEntity> handleNoResultException( 44 | NoResultException noResultException, HttpServletRequest request) { 45 | 46 | logger.info("> handleNoResultException"); 47 | 48 | ExceptionAttributes exceptionAttributes = new DefaultExceptionAttributes(); 49 | 50 | Map responseBody = exceptionAttributes 51 | .getExceptionAttributes(noResultException, request, 52 | HttpStatus.NOT_FOUND); 53 | 54 | logger.info("< handleNoResultException"); 55 | return new ResponseEntity>(responseBody, 56 | HttpStatus.NOT_FOUND); 57 | } 58 | 59 | /** 60 | * Handles all Exceptions not addressed by more specific 61 | * @ExceptionHandler methods. Creates a response with the 62 | * Exception Attributes in the response body as JSON and a HTTP status code 63 | * of 500, internal server error. 64 | * 65 | * @param exception An Exception instance. 66 | * @param request The HttpServletRequest in which the Exception was raised. 67 | * @return A ResponseEntity containing the Exception Attributes in the body 68 | * and a HTTP status code 500. 69 | */ 70 | @ExceptionHandler(Exception.class) 71 | public ResponseEntity> handleException( 72 | Exception exception, HttpServletRequest request) { 73 | 74 | logger.error("> handleException"); 75 | logger.error("- Exception: ", exception); 76 | 77 | ExceptionAttributes exceptionAttributes = new DefaultExceptionAttributes(); 78 | 79 | Map responseBody = exceptionAttributes 80 | .getExceptionAttributes(exception, request, 81 | HttpStatus.INTERNAL_SERVER_ERROR); 82 | 83 | logger.error("< handleException"); 84 | return new ResponseEntity>(responseBody, 85 | HttpStatus.INTERNAL_SERVER_ERROR); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/api/GreetingController.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import java.util.Collection; 4 | import java.util.concurrent.Future; 5 | 6 | import org.example.ws.model.Greeting; 7 | import org.example.ws.service.EmailService; 8 | import org.example.ws.service.GreetingService; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestMethod; 17 | import org.springframework.web.bind.annotation.RequestParam; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | /** 21 | * The GreetingController class is a RESTful web service controller. The 22 | * @RestController annotation informs Spring that each 23 | * @RequestMapping method returns a @ResponseBody 24 | * which, by default, contains a ResponseEntity converted into JSON with an 25 | * associated HTTP status code. 26 | * 27 | * @author Matt Warman 28 | */ 29 | @RestController 30 | public class GreetingController extends BaseController { 31 | 32 | /** 33 | * The GreetingService business service. 34 | */ 35 | @Autowired 36 | private GreetingService greetingService; 37 | 38 | /** 39 | * The EmailService business service. 40 | */ 41 | @Autowired 42 | private EmailService emailService; 43 | 44 | /** 45 | * Web service endpoint to fetch all Greeting entities. The service returns 46 | * the collection of Greeting entities as JSON. 47 | * 48 | * @return A ResponseEntity containing a Collection of Greeting objects. 49 | */ 50 | @RequestMapping( 51 | value = "/api/greetings", 52 | method = RequestMethod.GET, 53 | produces = MediaType.APPLICATION_JSON_VALUE) 54 | public ResponseEntity> getGreetings() { 55 | logger.info("> getGreetings"); 56 | 57 | Collection greetings = greetingService.findAll(); 58 | 59 | logger.info("< getGreetings"); 60 | return new ResponseEntity>(greetings, 61 | HttpStatus.OK); 62 | } 63 | 64 | /** 65 | * Web service endpoint to fetch a single Greeting entity by primary key 66 | * identifier. 67 | * 68 | * If found, the Greeting is returned as JSON with HTTP status 200. 69 | * 70 | * If not found, the service returns an empty response body with HTTP status 71 | * 404. 72 | * 73 | * @param id A Long URL path variable containing the Greeting primary key 74 | * identifier. 75 | * @return A ResponseEntity containing a single Greeting object, if found, 76 | * and a HTTP status code as described in the method comment. 77 | */ 78 | @RequestMapping( 79 | value = "/api/greetings/{id}", 80 | method = RequestMethod.GET, 81 | produces = MediaType.APPLICATION_JSON_VALUE) 82 | public ResponseEntity getGreeting(@PathVariable("id") Long id) { 83 | logger.info("> getGreeting id:{}", id); 84 | 85 | Greeting greeting = greetingService.findOne(id); 86 | if (greeting == null) { 87 | return new ResponseEntity(HttpStatus.NOT_FOUND); 88 | } 89 | 90 | logger.info("< getGreeting id:{}", id); 91 | return new ResponseEntity(greeting, HttpStatus.OK); 92 | } 93 | 94 | /** 95 | * Web service endpoint to create a single Greeting entity. The HTTP request 96 | * body is expected to contain a Greeting object in JSON format. The 97 | * Greeting is persisted in the data repository. 98 | * 99 | * If created successfully, the persisted Greeting is returned as JSON with 100 | * HTTP status 201. 101 | * 102 | * If not created successfully, the service returns an empty response body 103 | * with HTTP status 500. 104 | * 105 | * @param greeting The Greeting object to be created. 106 | * @return A ResponseEntity containing a single Greeting object, if created 107 | * successfully, and a HTTP status code as described in the method 108 | * comment. 109 | */ 110 | @RequestMapping( 111 | value = "/api/greetings", 112 | method = RequestMethod.POST, 113 | consumes = MediaType.APPLICATION_JSON_VALUE, 114 | produces = MediaType.APPLICATION_JSON_VALUE) 115 | public ResponseEntity createGreeting( 116 | @RequestBody Greeting greeting) { 117 | logger.info("> createGreeting"); 118 | 119 | Greeting savedGreeting = greetingService.create(greeting); 120 | 121 | logger.info("< createGreeting"); 122 | return new ResponseEntity(savedGreeting, HttpStatus.CREATED); 123 | } 124 | 125 | /** 126 | * Web service endpoint to update a single Greeting entity. The HTTP request 127 | * body is expected to contain a Greeting object in JSON format. The 128 | * Greeting is updated in the data repository. 129 | * 130 | * If updated successfully, the persisted Greeting is returned as JSON with 131 | * HTTP status 200. 132 | * 133 | * If not found, the service returns an empty response body and HTTP status 134 | * 404. 135 | * 136 | * If not updated successfully, the service returns an empty response body 137 | * with HTTP status 500. 138 | * 139 | * @param greeting The Greeting object to be updated. 140 | * @return A ResponseEntity containing a single Greeting object, if updated 141 | * successfully, and a HTTP status code as described in the method 142 | * comment. 143 | */ 144 | @RequestMapping( 145 | value = "/api/greetings/{id}", 146 | method = RequestMethod.PUT, 147 | consumes = MediaType.APPLICATION_JSON_VALUE, 148 | produces = MediaType.APPLICATION_JSON_VALUE) 149 | public ResponseEntity updateGreeting( 150 | @RequestBody Greeting greeting) { 151 | logger.info("> updateGreeting id:{}", greeting.getId()); 152 | 153 | Greeting updatedGreeting = greetingService.update(greeting); 154 | if (updatedGreeting == null) { 155 | return new ResponseEntity( 156 | HttpStatus.INTERNAL_SERVER_ERROR); 157 | } 158 | 159 | logger.info("< updateGreeting id:{}", greeting.getId()); 160 | return new ResponseEntity(updatedGreeting, HttpStatus.OK); 161 | } 162 | 163 | /** 164 | * Web service endpoint to delete a single Greeting entity. The HTTP request 165 | * body is empty. The primary key identifier of the Greeting to be deleted 166 | * is supplied in the URL as a path variable. 167 | * 168 | * If deleted successfully, the service returns an empty response body with 169 | * HTTP status 204. 170 | * 171 | * If not deleted successfully, the service returns an empty response body 172 | * with HTTP status 500. 173 | * 174 | * @param id A Long URL path variable containing the Greeting primary key 175 | * identifier. 176 | * @return A ResponseEntity with an empty response body and a HTTP status 177 | * code as described in the method comment. 178 | */ 179 | @RequestMapping( 180 | value = "/api/greetings/{id}", 181 | method = RequestMethod.DELETE) 182 | public ResponseEntity deleteGreeting( 183 | @PathVariable("id") Long id) { 184 | logger.info("> deleteGreeting id:{}", id); 185 | 186 | greetingService.delete(id); 187 | 188 | logger.info("< deleteGreeting id:{}", id); 189 | return new ResponseEntity(HttpStatus.NO_CONTENT); 190 | } 191 | 192 | /** 193 | * Web service endpoint to fetch a single Greeting entity by primary key 194 | * identifier and send it as an email. 195 | * 196 | * If found, the Greeting is returned as JSON with HTTP status 200 and sent 197 | * via Email. 198 | * 199 | * If not found, the service returns an empty response body with HTTP status 200 | * 404. 201 | * 202 | * @param id A Long URL path variable containing the Greeting primary key 203 | * identifier. 204 | * @param waitForAsyncResult A boolean indicating if the web service should 205 | * wait for the asynchronous email transmission. 206 | * @return A ResponseEntity containing a single Greeting object, if found, 207 | * and a HTTP status code as described in the method comment. 208 | */ 209 | @RequestMapping( 210 | value = "/api/greetings/{id}/send", 211 | method = RequestMethod.POST, 212 | produces = MediaType.APPLICATION_JSON_VALUE) 213 | public ResponseEntity sendGreeting(@PathVariable("id") Long id, 214 | @RequestParam( 215 | value = "wait", 216 | defaultValue = "false") boolean waitForAsyncResult) { 217 | 218 | logger.info("> sendGreeting id:{}", id); 219 | 220 | Greeting greeting = null; 221 | 222 | try { 223 | greeting = greetingService.findOne(id); 224 | if (greeting == null) { 225 | logger.info("< sendGreeting id:{}", id); 226 | return new ResponseEntity(HttpStatus.NOT_FOUND); 227 | } 228 | 229 | if (waitForAsyncResult) { 230 | Future asyncResponse = emailService 231 | .sendAsyncWithResult(greeting); 232 | boolean emailSent = asyncResponse.get(); 233 | logger.info("- greeting email sent? {}", emailSent); 234 | } else { 235 | emailService.sendAsync(greeting); 236 | } 237 | } catch (Exception e) { 238 | logger.error("A problem occurred sending the Greeting.", e); 239 | return new ResponseEntity( 240 | HttpStatus.INTERNAL_SERVER_ERROR); 241 | } 242 | 243 | logger.info("< sendGreeting id:{}", id); 244 | return new ResponseEntity(greeting, HttpStatus.OK); 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/api/QuoteController.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import org.example.ws.service.quote.tss.Quote; 4 | import org.example.ws.service.quote.tss.QuoteService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | /** 14 | * The QuoteController class is a RESTful web service controller. The 15 | * @RestController annotation informs Spring that each 16 | * @RequestMapping method returns a @ResponseBody 17 | * which, by default, contains a ResponseEntity converted into JSON with an 18 | * associated HTTP status code. 19 | * 20 | * @author Matt Warman 21 | */ 22 | @RestController 23 | public class QuoteController extends BaseController { 24 | 25 | /** 26 | * The QuoteService business service. 27 | */ 28 | @Autowired 29 | private QuoteService quoteService; 30 | 31 | /** 32 | * Web service endpoint to fetch the Quote of the day. 33 | * 34 | * If found, the Quote is returned as JSON with HTTP status 200. 35 | * 36 | * If not found, the service returns an empty response body with HTTP status 37 | * 404. 38 | * 39 | * @return A ResponseEntity containing a single Quote object, if found, and 40 | * a HTTP status code as described in the method comment. 41 | */ 42 | @RequestMapping( 43 | value = "/api/quotes/daily", 44 | method = RequestMethod.GET, 45 | produces = MediaType.APPLICATION_JSON_VALUE) 46 | public ResponseEntity getQuoteOfTheDay() { 47 | logger.info("> getQuoteOfTheDay"); 48 | 49 | Quote quote = quoteService.getDaily(QuoteService.CATEGORY_INSPIRATIONAL); 50 | 51 | if (quote == null) { 52 | return new ResponseEntity(HttpStatus.NOT_FOUND); 53 | } 54 | logger.info("< getQuoteOfTheDay"); 55 | return new ResponseEntity(quote, HttpStatus.OK); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/config/application-batch.properties: -------------------------------------------------------------------------------- 1 | ### 2 | # The Batch Application Configuration File 3 | # 4 | # This file is included when the 'batch' Spring profile is active. 5 | ### 6 | 7 | ## 8 | # Greeting Scheduled Process Configuration 9 | ## 10 | batch.greeting.fixedrate=15000 11 | batch.greeting.fixeddelay=15000 12 | batch.greeting.initialdelay=5000 13 | batch.greeting.cron=0,30 * * * * * 14 | -------------------------------------------------------------------------------- /src/main/resources/config/application.properties: -------------------------------------------------------------------------------- 1 | ### 2 | # The Base Application Configuration File 3 | ### 4 | 5 | ### 6 | # Profile Configuration 7 | # available profiles: batch 8 | ### 9 | spring.profiles.active=batch 10 | 11 | ### 12 | # Data Source Configuration 13 | ### 14 | 15 | # Hibernate 16 | spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 17 | spring.jpa.hibernate.ddl-auto=validate 18 | 19 | # Initialization 20 | spring.datasource.schema=classpath:/data/hsqldb/schema.sql 21 | spring.datasource.data=classpath:/data/hsqldb/data.sql 22 | 23 | ### 24 | # Cache Configuration 25 | ### 26 | spring.cache.cache-names=greetings 27 | spring.cache.caffeine.spec=maximumSize=250,expireAfterAccess=600s 28 | 29 | ### 30 | # Actuator Configuration 31 | ### 32 | endpoints.health.id=status 33 | endpoints.health.sensitive=false 34 | 35 | endpoints.shutdown.enabled=true 36 | endpoints.shutdown.sensitive=false 37 | 38 | management.context-path=/actuators 39 | 40 | management.security.roles=SYSADMIN 41 | 42 | ### 43 | # Spring Security Configuration 44 | ### 45 | security.user.name=leanstacks 46 | security.user.password=s3cur!T 47 | security.user.role=USER,SYSADMIN 48 | 49 | ### 50 | # Logging Configuration 51 | ### 52 | logging.level.org.example.ws=DEBUG 53 | logging.level.org.springframework.web.client=DEBUG 54 | -------------------------------------------------------------------------------- /src/main/resources/data/hsqldb/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO Greeting (text) VALUES ('Hello World!'); 2 | INSERT INTO Greeting (text) VALUES ('Hola Mundo!'); 3 | -------------------------------------------------------------------------------- /src/main/resources/data/hsqldb/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE Greeting IF EXISTS; 2 | 3 | CREATE TABLE Greeting ( 4 | id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL, 5 | text VARCHAR(100) NOT NULL, 6 | PRIMARY KEY(id) 7 | ); 8 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/AbstractControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws; 2 | 3 | import java.io.IOException; 4 | 5 | import org.example.ws.web.api.BaseController; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 10 | import org.springframework.web.context.WebApplicationContext; 11 | 12 | import com.fasterxml.jackson.core.JsonParseException; 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.JsonMappingException; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | 17 | /** 18 | * This class extends the functionality of AbstractTest. AbstractControllerTest 19 | * is the parent of all web controller unit test classes. The class ensures that 20 | * a type of WebApplicationContext is built and prepares a MockMvc instance for 21 | * use in test methods. 22 | * 23 | * @author Matt Warman 24 | */ 25 | @WebAppConfiguration 26 | public abstract class AbstractControllerTest extends AbstractTest { 27 | 28 | protected MockMvc mvc; 29 | 30 | @Autowired 31 | protected WebApplicationContext webApplicationContext; 32 | 33 | /** 34 | * Prepares the test class for execution of web tests. Builds a MockMvc 35 | * instance. Call this method from the concrete JUnit test class in the 36 | * @Before setup method. 37 | */ 38 | protected void setUp() { 39 | mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 40 | } 41 | 42 | /** 43 | * Prepares the test class for execution of web tests. Builds a MockMvc 44 | * instance using standalone configuration facilitating the injection of 45 | * Mockito resources into the controller class. 46 | * @param controller A controller object to be tested. 47 | */ 48 | protected void setUp(BaseController controller) { 49 | mvc = MockMvcBuilders.standaloneSetup(controller).build(); 50 | } 51 | 52 | /** 53 | * Maps an Object into a JSON String. Uses a Jackson ObjectMapper. 54 | * @param obj The Object to map. 55 | * @return A String of JSON. 56 | * @throws JsonProcessingException Thrown if an error occurs while mapping. 57 | */ 58 | protected String mapToJson(Object obj) throws JsonProcessingException { 59 | ObjectMapper mapper = new ObjectMapper(); 60 | return mapper.writeValueAsString(obj); 61 | } 62 | 63 | /** 64 | * Maps a String of JSON into an instance of a Class of type T. Uses a 65 | * Jackson ObjectMapper. 66 | * @param json A String of JSON. 67 | * @param clazz A Class of type T. The mapper will attempt to convert the 68 | * JSON into an Object of this Class type. 69 | * @return An Object of type T. 70 | * @throws JsonParseException Thrown if an error occurs while mapping. 71 | * @throws JsonMappingException Thrown if an error occurs while mapping. 72 | * @throws IOException Thrown if an error occurs while mapping. 73 | */ 74 | protected T mapFromJson(String json, Class clazz) 75 | throws JsonParseException, JsonMappingException, IOException { 76 | ObjectMapper mapper = new ObjectMapper(); 77 | return mapper.readValue(json, clazz); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/AbstractTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | 9 | /** 10 | * The AbstractTest class is the parent of all JUnit test classes. This class 11 | * configures the test ApplicationContext and test runner environment. 12 | * 13 | * @author Matt Warman 14 | */ 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest( 17 | classes = Application.class) 18 | public abstract class AbstractTest { 19 | 20 | /** 21 | * The Logger instance for all classes in the unit test framework. 22 | */ 23 | protected Logger logger = LoggerFactory.getLogger(this.getClass()); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/service/GreetingServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.persistence.EntityExistsException; 6 | import javax.persistence.NoResultException; 7 | 8 | import org.example.ws.AbstractTest; 9 | import org.example.ws.model.Greeting; 10 | import org.junit.After; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | /** 18 | * Unit test methods for the GreetingService and GreetingServiceBean. 19 | * 20 | * @author Matt Warman 21 | */ 22 | @Transactional 23 | public class GreetingServiceTest extends AbstractTest { 24 | 25 | @Autowired 26 | private GreetingService service; 27 | 28 | @Before 29 | public void setUp() { 30 | service.evictCache(); 31 | } 32 | 33 | @After 34 | public void tearDown() { 35 | // clean up after each test method 36 | } 37 | 38 | @Test 39 | public void testFindAll() { 40 | 41 | Collection list = service.findAll(); 42 | 43 | Assert.assertNotNull("failure - expected not null", list); 44 | Assert.assertEquals("failure - expected list size", 2, list.size()); 45 | 46 | } 47 | 48 | @Test 49 | public void testFindOne() { 50 | 51 | Long id = new Long(1); 52 | 53 | Greeting entity = service.findOne(id); 54 | 55 | Assert.assertNotNull("failure - expected not null", entity); 56 | Assert.assertEquals("failure - expected id attribute match", id, 57 | entity.getId()); 58 | 59 | } 60 | 61 | @Test 62 | public void testFindOneNotFound() { 63 | 64 | Long id = Long.MAX_VALUE; 65 | 66 | Greeting entity = service.findOne(id); 67 | 68 | Assert.assertNull("failure - expected null", entity); 69 | 70 | } 71 | 72 | @Test 73 | public void testCreate() { 74 | 75 | Greeting entity = new Greeting(); 76 | entity.setText("test"); 77 | 78 | Greeting createdEntity = service.create(entity); 79 | 80 | Assert.assertNotNull("failure - expected not null", createdEntity); 81 | Assert.assertNotNull("failure - expected id attribute not null", 82 | createdEntity.getId()); 83 | Assert.assertEquals("failure - expected text attribute match", "test", 84 | createdEntity.getText()); 85 | 86 | Collection list = service.findAll(); 87 | 88 | Assert.assertEquals("failure - expected size", 3, list.size()); 89 | 90 | } 91 | 92 | @Test 93 | public void testCreateWithId() { 94 | 95 | Exception exception = null; 96 | 97 | Greeting entity = new Greeting(); 98 | entity.setId(Long.MAX_VALUE); 99 | entity.setText("test"); 100 | 101 | try { 102 | service.create(entity); 103 | } catch (EntityExistsException e) { 104 | exception = e; 105 | } 106 | 107 | Assert.assertNotNull("failure - expected exception", exception); 108 | Assert.assertTrue("failure - expected EntityExistsException", 109 | exception instanceof EntityExistsException); 110 | 111 | } 112 | 113 | @Test 114 | public void testUpdate() { 115 | 116 | Long id = new Long(1); 117 | 118 | Greeting entity = service.findOne(id); 119 | 120 | Assert.assertNotNull("failure - expected not null", entity); 121 | 122 | String updatedText = entity.getText() + " test"; 123 | entity.setText(updatedText); 124 | Greeting updatedEntity = service.update(entity); 125 | 126 | Assert.assertNotNull("failure - expected not null", updatedEntity); 127 | Assert.assertEquals("failure - expected id attribute match", id, 128 | updatedEntity.getId()); 129 | Assert.assertEquals("failure - expected text attribute match", 130 | updatedText, updatedEntity.getText()); 131 | 132 | } 133 | 134 | @Test 135 | public void testUpdateNotFound() { 136 | 137 | Exception exception = null; 138 | 139 | Greeting entity = new Greeting(); 140 | entity.setId(Long.MAX_VALUE); 141 | entity.setText("test"); 142 | 143 | try { 144 | service.update(entity); 145 | } catch (NoResultException e) { 146 | exception = e; 147 | } 148 | 149 | Assert.assertNotNull("failure - expected exception", exception); 150 | Assert.assertTrue("failure - expected NoResultException", 151 | exception instanceof NoResultException); 152 | 153 | } 154 | 155 | @Test 156 | public void testDelete() { 157 | 158 | Long id = new Long(1); 159 | 160 | Greeting entity = service.findOne(id); 161 | 162 | Assert.assertNotNull("failure - expected not null", entity); 163 | 164 | service.delete(id); 165 | 166 | Collection list = service.findAll(); 167 | 168 | Assert.assertEquals("failure - expected size", 1, list.size()); 169 | 170 | Greeting deletedEntity = service.findOne(id); 171 | 172 | Assert.assertNull("failure - expected null", deletedEntity); 173 | 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/web/api/GreetingControllerMocksTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import static org.mockito.Matchers.any; 4 | import static org.mockito.Mockito.times; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | 11 | import org.example.ws.AbstractControllerTest; 12 | import org.example.ws.model.Greeting; 13 | import org.example.ws.service.EmailService; 14 | import org.example.ws.service.GreetingService; 15 | import org.junit.Assert; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.mockito.InjectMocks; 19 | import org.mockito.Mock; 20 | import org.mockito.MockitoAnnotations; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.web.servlet.MvcResult; 23 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 24 | import org.springframework.transaction.annotation.Transactional; 25 | 26 | /** 27 | * Unit tests for the GreetingController using Mockito mocks and spies. 28 | * 29 | * These tests utilize the Mockito framework objects to simulate interaction 30 | * with back-end components. The controller methods are invoked directly 31 | * bypassing the Spring MVC mappings. Back-end components are mocked and 32 | * injected into the controller. Mockito spies and verifications are performed 33 | * ensuring controller behaviors. 34 | * 35 | * @author Matt Warman 36 | */ 37 | @Transactional 38 | public class GreetingControllerMocksTest extends AbstractControllerTest { 39 | 40 | /** 41 | * A mocked GreetingService 42 | */ 43 | @Mock 44 | private GreetingService greetingService; 45 | 46 | /** 47 | * A mocked EmailService 48 | */ 49 | @Mock 50 | private EmailService emailService; 51 | 52 | /** 53 | * A GreetingController instance with @Mock components injected 54 | * into it. 55 | */ 56 | @InjectMocks 57 | private GreetingController greetingController; 58 | 59 | /** 60 | * Setup each test method. Initialize Mockito mock and spy objects. Scan for 61 | * Mockito annotations. 62 | */ 63 | @Before 64 | public void setUp() { 65 | // Initialize Mockito annotated components 66 | MockitoAnnotations.initMocks(this); 67 | // Prepare the Spring MVC Mock components for standalone testing 68 | setUp(greetingController); 69 | } 70 | 71 | @Test 72 | public void testGetGreetings() throws Exception { 73 | 74 | // Create some test data 75 | Collection list = getEntityListStubData(); 76 | 77 | // Stub the GreetingService.findAll method return value 78 | when(greetingService.findAll()).thenReturn(list); 79 | 80 | // Perform the behavior being tested 81 | String uri = "/api/greetings"; 82 | 83 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri) 84 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 85 | 86 | // Extract the response status and body 87 | String content = result.getResponse().getContentAsString(); 88 | int status = result.getResponse().getStatus(); 89 | 90 | // Verify the GreetingService.findAll method was invoked once 91 | verify(greetingService, times(1)).findAll(); 92 | 93 | // Perform standard JUnit assertions on the response 94 | Assert.assertEquals("failure - expected HTTP status 200", 200, status); 95 | Assert.assertTrue( 96 | "failure - expected HTTP response body to have a value", 97 | content.trim().length() > 0); 98 | 99 | } 100 | 101 | @Test 102 | public void testGetGreeting() throws Exception { 103 | 104 | // Create some test data 105 | Long id = new Long(1); 106 | Greeting entity = getEntityStubData(); 107 | 108 | // Stub the GreetingService.findOne method return value 109 | when(greetingService.findOne(id)).thenReturn(entity); 110 | 111 | // Perform the behavior being tested 112 | String uri = "/api/greetings/{id}"; 113 | 114 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri, id) 115 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 116 | 117 | // Extract the response status and body 118 | String content = result.getResponse().getContentAsString(); 119 | int status = result.getResponse().getStatus(); 120 | 121 | // Verify the GreetingService.findOne method was invoked once 122 | verify(greetingService, times(1)).findOne(id); 123 | 124 | // Perform standard JUnit assertions on the test results 125 | Assert.assertEquals("failure - expected HTTP status 200", 200, status); 126 | Assert.assertTrue( 127 | "failure - expected HTTP response body to have a value", 128 | content.trim().length() > 0); 129 | } 130 | 131 | @Test 132 | public void testGetGreetingNotFound() throws Exception { 133 | 134 | // Create some test data 135 | Long id = Long.MAX_VALUE; 136 | 137 | // Stub the GreetingService.findOne method return value 138 | when(greetingService.findOne(id)).thenReturn(null); 139 | 140 | // Perform the behavior being tested 141 | String uri = "/api/greetings/{id}"; 142 | 143 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri, id) 144 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 145 | 146 | // Extract the response status and body 147 | String content = result.getResponse().getContentAsString(); 148 | int status = result.getResponse().getStatus(); 149 | 150 | // Verify the GreetingService.findOne method was invoked once 151 | verify(greetingService, times(1)).findOne(id); 152 | 153 | // Perform standard JUnit assertions on the test results 154 | Assert.assertEquals("failure - expected HTTP status 404", 404, status); 155 | Assert.assertTrue("failure - expected HTTP response body to be empty", 156 | content.trim().length() == 0); 157 | 158 | } 159 | 160 | @Test 161 | public void testCreateGreeting() throws Exception { 162 | 163 | // Create some test data 164 | Greeting entity = getEntityStubData(); 165 | 166 | // Stub the GreetingService.create method return value 167 | when(greetingService.create(any(Greeting.class))).thenReturn(entity); 168 | 169 | // Perform the behavior being tested 170 | String uri = "/api/greetings"; 171 | String inputJson = super.mapToJson(entity); 172 | 173 | MvcResult result = mvc 174 | .perform(MockMvcRequestBuilders.post(uri) 175 | .contentType(MediaType.APPLICATION_JSON) 176 | .accept(MediaType.APPLICATION_JSON).content(inputJson)) 177 | .andReturn(); 178 | 179 | // Extract the response status and body 180 | String content = result.getResponse().getContentAsString(); 181 | int status = result.getResponse().getStatus(); 182 | 183 | // Verify the GreetingService.create method was invoked once 184 | verify(greetingService, times(1)).create(any(Greeting.class)); 185 | 186 | // Perform standard JUnit assertions on the test results 187 | Assert.assertEquals("failure - expected HTTP status 201", 201, status); 188 | Assert.assertTrue( 189 | "failure - expected HTTP response body to have a value", 190 | content.trim().length() > 0); 191 | 192 | Greeting createdEntity = super.mapFromJson(content, Greeting.class); 193 | 194 | Assert.assertNotNull("failure - expected entity not null", 195 | createdEntity); 196 | Assert.assertNotNull("failure - expected id attribute not null", 197 | createdEntity.getId()); 198 | Assert.assertEquals("failure - expected text attribute match", 199 | entity.getText(), createdEntity.getText()); 200 | } 201 | 202 | @Test 203 | public void testUpdateGreeting() throws Exception { 204 | 205 | // Create some test data 206 | Greeting entity = getEntityStubData(); 207 | entity.setText(entity.getText() + " test"); 208 | Long id = new Long(1); 209 | 210 | // Stub the GreetingService.update method return value 211 | when(greetingService.update(any(Greeting.class))).thenReturn(entity); 212 | 213 | // Perform the behavior being tested 214 | String uri = "/api/greetings/{id}"; 215 | String inputJson = super.mapToJson(entity); 216 | 217 | MvcResult result = mvc 218 | .perform(MockMvcRequestBuilders.put(uri, id) 219 | .contentType(MediaType.APPLICATION_JSON) 220 | .accept(MediaType.APPLICATION_JSON).content(inputJson)) 221 | .andReturn(); 222 | 223 | // Extract the response status and body 224 | String content = result.getResponse().getContentAsString(); 225 | int status = result.getResponse().getStatus(); 226 | 227 | // Verify the GreetingService.update method was invoked once 228 | verify(greetingService, times(1)).update(any(Greeting.class)); 229 | 230 | // Perform standard JUnit assertions on the test results 231 | Assert.assertEquals("failure - expected HTTP status 200", 200, status); 232 | Assert.assertTrue( 233 | "failure - expected HTTP response body to have a value", 234 | content.trim().length() > 0); 235 | 236 | Greeting updatedEntity = super.mapFromJson(content, Greeting.class); 237 | 238 | Assert.assertNotNull("failure - expected entity not null", 239 | updatedEntity); 240 | Assert.assertEquals("failure - expected id attribute unchanged", 241 | entity.getId(), updatedEntity.getId()); 242 | Assert.assertEquals("failure - expected text attribute match", 243 | entity.getText(), updatedEntity.getText()); 244 | 245 | } 246 | 247 | @Test 248 | public void testDeleteGreeting() throws Exception { 249 | 250 | // Create some test data 251 | Long id = new Long(1); 252 | 253 | // Perform the behavior being tested 254 | String uri = "/api/greetings/{id}"; 255 | 256 | MvcResult result = mvc.perform(MockMvcRequestBuilders.delete(uri, id)) 257 | .andReturn(); 258 | 259 | // Extract the response status and body 260 | String content = result.getResponse().getContentAsString(); 261 | int status = result.getResponse().getStatus(); 262 | 263 | // Verify the GreetingService.delete method was invoked once 264 | verify(greetingService, times(1)).delete(id); 265 | 266 | // Perform standard JUnit assertions on the test results 267 | Assert.assertEquals("failure - expected HTTP status 204", 204, status); 268 | Assert.assertTrue("failure - expected HTTP response body to be empty", 269 | content.trim().length() == 0); 270 | 271 | } 272 | 273 | private Collection getEntityListStubData() { 274 | Collection list = new ArrayList(); 275 | list.add(getEntityStubData()); 276 | return list; 277 | } 278 | 279 | private Greeting getEntityStubData() { 280 | Greeting entity = new Greeting(); 281 | entity.setId(1L); 282 | entity.setText("hello"); 283 | return entity; 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/web/api/GreetingControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import org.example.ws.AbstractControllerTest; 4 | import org.example.ws.model.Greeting; 5 | import org.example.ws.service.GreetingService; 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.web.servlet.MvcResult; 12 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | /** 16 | * Unit tests for the GreetingController using Spring MVC Mocks. 17 | * 18 | * These tests utilize the Spring MVC Mock objects to simulate sending actual 19 | * HTTP requests to the Controller component. This test ensures that the 20 | * RequestMappings are configured correctly. Also, these tests ensure that the 21 | * request and response bodies are serialized as expected. 22 | * 23 | * @author Matt Warman 24 | */ 25 | @Transactional 26 | public class GreetingControllerTest extends AbstractControllerTest { 27 | 28 | @Autowired 29 | private GreetingService greetingService; 30 | 31 | @Before 32 | public void setUp() { 33 | super.setUp(); 34 | greetingService.evictCache(); 35 | } 36 | 37 | @Test 38 | public void testGetGreetings() throws Exception { 39 | 40 | String uri = "/api/greetings"; 41 | 42 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri) 43 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 44 | 45 | String content = result.getResponse().getContentAsString(); 46 | int status = result.getResponse().getStatus(); 47 | 48 | Assert.assertEquals("failure - expected HTTP status", 200, status); 49 | Assert.assertTrue( 50 | "failure - expected HTTP response body to have a value", 51 | content.trim().length() > 0); 52 | 53 | } 54 | 55 | @Test 56 | public void testGetGreeting() throws Exception { 57 | 58 | String uri = "/api/greetings/{id}"; 59 | Long id = new Long(1); 60 | 61 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri, id) 62 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 63 | 64 | String content = result.getResponse().getContentAsString(); 65 | int status = result.getResponse().getStatus(); 66 | 67 | Assert.assertEquals("failure - expected HTTP status 200", 200, status); 68 | Assert.assertTrue( 69 | "failure - expected HTTP response body to have a value", 70 | content.trim().length() > 0); 71 | 72 | } 73 | 74 | @Test 75 | public void testGetGreetingNotFound() throws Exception { 76 | 77 | String uri = "/api/greetings/{id}"; 78 | Long id = Long.MAX_VALUE; 79 | 80 | MvcResult result = mvc.perform(MockMvcRequestBuilders.get(uri, id) 81 | .accept(MediaType.APPLICATION_JSON)).andReturn(); 82 | 83 | String content = result.getResponse().getContentAsString(); 84 | int status = result.getResponse().getStatus(); 85 | 86 | Assert.assertEquals("failure - expected HTTP status 404", 404, status); 87 | Assert.assertTrue("failure - expected HTTP response body to be empty", 88 | content.trim().length() == 0); 89 | 90 | } 91 | 92 | @Test 93 | public void testCreateGreeting() throws Exception { 94 | 95 | String uri = "/api/greetings"; 96 | Greeting greeting = new Greeting(); 97 | greeting.setText("test"); 98 | String inputJson = super.mapToJson(greeting); 99 | 100 | MvcResult result = mvc 101 | .perform(MockMvcRequestBuilders.post(uri) 102 | .contentType(MediaType.APPLICATION_JSON) 103 | .accept(MediaType.APPLICATION_JSON).content(inputJson)) 104 | .andReturn(); 105 | 106 | String content = result.getResponse().getContentAsString(); 107 | int status = result.getResponse().getStatus(); 108 | 109 | Assert.assertEquals("failure - expected HTTP status 201", 201, status); 110 | Assert.assertTrue( 111 | "failure - expected HTTP response body to have a value", 112 | content.trim().length() > 0); 113 | 114 | Greeting createdGreeting = super.mapFromJson(content, Greeting.class); 115 | 116 | Assert.assertNotNull("failure - expected greeting not null", 117 | createdGreeting); 118 | Assert.assertNotNull("failure - expected greeting.id not null", 119 | createdGreeting.getId()); 120 | Assert.assertEquals("failure - expected greeting.text match", "test", 121 | createdGreeting.getText()); 122 | 123 | } 124 | 125 | @Test 126 | public void testUpdateGreeting() throws Exception { 127 | 128 | String uri = "/api/greetings/{id}"; 129 | Long id = new Long(1); 130 | Greeting greeting = greetingService.findOne(id); 131 | String updatedText = greeting.getText() + " test"; 132 | greeting.setText(updatedText); 133 | String inputJson = super.mapToJson(greeting); 134 | 135 | MvcResult result = mvc 136 | .perform(MockMvcRequestBuilders.put(uri, id) 137 | .contentType(MediaType.APPLICATION_JSON) 138 | .accept(MediaType.APPLICATION_JSON).content(inputJson)) 139 | .andReturn(); 140 | 141 | String content = result.getResponse().getContentAsString(); 142 | int status = result.getResponse().getStatus(); 143 | 144 | Assert.assertEquals("failure - expected HTTP status 200", 200, status); 145 | Assert.assertTrue( 146 | "failure - expected HTTP response body to have a value", 147 | content.trim().length() > 0); 148 | 149 | Greeting updatedGreeting = super.mapFromJson(content, Greeting.class); 150 | 151 | Assert.assertNotNull("failure - expected greeting not null", 152 | updatedGreeting); 153 | Assert.assertEquals("failure - expected greeting.id unchanged", 154 | greeting.getId(), updatedGreeting.getId()); 155 | Assert.assertEquals("failure - expected updated greeting text match", 156 | updatedText, updatedGreeting.getText()); 157 | 158 | } 159 | 160 | @Test 161 | public void testDeleteGreeting() throws Exception { 162 | 163 | String uri = "/api/greetings/{id}"; 164 | Long id = new Long(1); 165 | 166 | MvcResult result = mvc.perform(MockMvcRequestBuilders.delete(uri, id) 167 | .contentType(MediaType.APPLICATION_JSON)).andReturn(); 168 | 169 | String content = result.getResponse().getContentAsString(); 170 | int status = result.getResponse().getStatus(); 171 | 172 | Assert.assertEquals("failure - expected HTTP status 204", 204, status); 173 | Assert.assertTrue("failure - expected HTTP response body to be empty", 174 | content.trim().length() == 0); 175 | 176 | Greeting deletedGreeting = greetingService.findOne(id); 177 | 178 | Assert.assertNull("failure - expected greeting to be null", 179 | deletedGreeting); 180 | 181 | } 182 | 183 | } 184 | --------------------------------------------------------------------------------