├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── example │ │ └── ws │ │ ├── Application.java │ │ ├── SecurityConfiguration.java │ │ ├── actuator │ │ └── health │ │ │ └── GreetingHealthIndicator.java │ │ ├── batch │ │ └── GreetingBatchBean.java │ │ ├── model │ │ ├── Account.java │ │ ├── Greeting.java │ │ ├── ReferenceEntity.java │ │ ├── Role.java │ │ └── TransactionalEntity.java │ │ ├── repository │ │ ├── AccountRepository.java │ │ ├── GreetingRepository.java │ │ └── RoleRepository.java │ │ ├── security │ │ ├── AccountAuthenticationProvider.java │ │ └── AccountUserDetailsService.java │ │ ├── service │ │ ├── AccountService.java │ │ ├── AccountServiceBean.java │ │ ├── EmailService.java │ │ ├── EmailServiceBean.java │ │ ├── GreetingService.java │ │ └── GreetingServiceBean.java │ │ ├── util │ │ ├── AsyncResponse.java │ │ └── RequestContext.java │ │ └── web │ │ ├── DefaultExceptionAttributes.java │ │ ├── ExceptionAttributes.java │ │ ├── api │ │ ├── BaseController.java │ │ ├── GreetingController.java │ │ └── RoleController.java │ │ └── filter │ │ └── RequestContextInitializationFilter.java └── resources │ ├── config │ ├── application-batch.properties │ ├── application-hsqldb.properties │ ├── application-mysql.properties │ └── application.properties │ └── data │ ├── changelog │ ├── db.changelog-0.0.1.xml │ ├── db.changelog-0.1.0.xml │ └── db.changelog-master.xml │ ├── hsqldb │ └── migrations │ │ ├── V0_0_1__initialize.sql │ │ └── V0_1_0__migration.sql │ └── mysql │ └── migrations │ ├── V0_0_1__initialize.sql │ └── V0_1_0__migration.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 | 7 | # Generated Directories and Files 8 | /target 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | -------------------------------------------------------------------------------- /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 Data 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 Data Fundamentals](https://www.youtube.com/playlist?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 14 | 15 | ### Repository Organization 16 | 17 | Each episode of the Spring Data Fundamentals video series has a corresponding branch in this repository. For example, all of the source code illustrated in the episode entitled [Abstracting Common Transactional Attributes into a Mapped Superclass with Spring Data JPA](https://youtu.be/_s6THdyyfN8?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc) may be found on the repository branch named [transactional-entity](https://github.com/mwarman/spring-data-fundamentals/tree/transactional-entity). 18 | 19 | ### Branches 20 | 21 | #### transactional-entity 22 | 23 | The branch named `transactional-entity` contains the source code illustrated in the episode [Abstracting Common Transactional Attributes into a Mapped Superclass with Spring Data JPA](https://youtu.be/_s6THdyyfN8?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 24 | 25 | #### reference-entity 26 | 27 | The branch named `reference-entity` contains the source code illustrated in the episode [Abstracting Common Reference Data Attributes into a Mapped Superclass with Spring Data JPA](https://youtu.be/xfgwrJmF8nY?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 28 | 29 | #### joda 30 | 31 | The branch named `joda` contains the source code illustrated in the episode [Using JODA Framework Classes as JPA Entity Model Attributes with Spring Data JPA](https://youtu.be/OcKtf_-K5cc?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 32 | 33 | #### jpa-query-definition 34 | 35 | The branch named `jpa-query-definition` contains the source code illustrated in the episode [Exploring Spring Data JPA Query Definition Strategies](https://youtu.be/S5vZP_03ENY?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 36 | 37 | #### jpa-mysql 38 | 39 | The branch named `jpa-mysql` contains the source code illustrated in the episode [Using a MySQL Database with Spring Data JPA](https://youtu.be/wjpeKiTiuRE?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 40 | 41 | #### flyway 42 | 43 | The branch named `flyway` contains the source code illustrated in the episode [Using Flyway with Spring Boot for Database Migrations](https://youtu.be/5JUJHHc4KZc?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 44 | 45 | #### liquibase 46 | 47 | The branch named `liquibase` contains the source code illustrated in the episode [Using Liquibase with Spring Boot for Database Migrations](https://youtu.be/7VeODrRkHXg?list=PLGDwUiT1wr693flGbjtm0WoB_722X6lNc). 48 | 49 | 50 | ## Languages 51 | 52 | This project is authored in Java. 53 | 54 | ## Installation 55 | 56 | ### Fork the Repository 57 | 58 | Fork the [Spring Data Fundamentals](https://github.com/mwarman/spring-data-fundamentals) repository on GitHub. Clone the project to your host machine. 59 | 60 | ### Dependencies 61 | 62 | The project requires the following dependencies be installed on the host machine: 63 | 64 | * Java Development Kit 8 or later 65 | * Apache Maven 3 or later 66 | 67 | ## Running 68 | 69 | The project uses [Maven](http://maven.apache.org/) for build, package, and test workflow automation. The following Maven goals are the most commonly used. 70 | 71 | ### spring-boot:run 72 | 73 | The `spring-boot:run` Maven goal performs the following workflow steps: 74 | 75 | * compiles Java classes to the /target directory 76 | * copies all resources to the /target directory 77 | * starts an embedded Apache Tomcat server 78 | 79 | To execute the `spring-boot:run` Maven goal, type the following command at a terminal prompt in the project base directory. 80 | 81 | ``` 82 | mvn spring-boot:run 83 | ``` 84 | 85 | Type `ctrl-C` to halt the web server. 86 | 87 | This goal is used for local machine development and functional testing. Use the `package` goal for server deployment. 88 | 89 | ### test 90 | 91 | The `test` Maven goal performs the following workflow steps: 92 | 93 | * compiles Java classes to the /target directory 94 | * copies all resources to the /target directory 95 | * executes the unit test suites 96 | * produces unit test reports 97 | 98 | 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. 99 | 100 | To execute the `test` Maven goal, type the following command at a terminal prompt in the project base directory. 101 | 102 | ``` 103 | mvn clean test 104 | ``` 105 | 106 | ### package 107 | 108 | The `package` Maven goal performs the following workflow steps: 109 | 110 | * compiles Java classes to the /target directory 111 | * copies all resources to the /target directory 112 | * executes the unit test suites 113 | * produces unit test reports 114 | * prepares an executable JAR file in the /target directory 115 | 116 | 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. 117 | 118 | To execute the `package` goal, type the following command at a terminal prompt in the project base directory. 119 | 120 | ``` 121 | mvn clean package 122 | ``` 123 | 124 | 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: 125 | 126 | ``` 127 | java -jar example-1.0.0.jar 128 | ``` 129 | 130 | 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: 131 | 132 | ``` 133 | java -jar example-1.0.0.jar --spring.profiles.active=mysql,batch 134 | ``` 135 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | 6 | org.example 7 | spring-data-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.jadira.usertype 41 | usertype.extended 42 | 5.0.0.GA 43 | 44 | 45 | org.flywaydb 46 | flyway-core 47 | 48 | 49 | org.liquibase 50 | liquibase-core 51 | 52 | 53 | org.hsqldb 54 | hsqldb 55 | runtime 56 | 57 | 58 | mysql 59 | mysql-connector-java 60 | runtime 61 | 62 | 63 | 64 | 65 | org.springframework 66 | spring-context-support 67 | 68 | 69 | com.github.ben-manes.caffeine 70 | caffeine 71 | 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-actuator 77 | 78 | 79 | 80 | 81 | com.google.guava 82 | guava 83 | 19.0 84 | 85 | 86 | joda-time 87 | joda-time 88 | 89 | 90 | com.fasterxml.jackson.datatype 91 | jackson-datatype-joda 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-starter-test 98 | test 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.springframework.boot 106 | spring-boot-maven-plugin 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /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/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.example.ws; 2 | 3 | import org.example.ws.security.AccountAuthenticationProvider; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.annotation.Order; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.config.http.SessionCreationPolicy; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | 16 | /** 17 | * The SecurityConfiguration class provides a centralized location for 18 | * application security configuration. This class bootstraps the Spring Security 19 | * components during application startup. 20 | * 21 | * @author Matt Warman 22 | */ 23 | @Configuration 24 | @EnableWebSecurity 25 | public class SecurityConfiguration { 26 | 27 | /** 28 | * The AccountAuthenticationProvider security component. 29 | */ 30 | @Autowired 31 | private AccountAuthenticationProvider accountAuthenticationProvider; 32 | 33 | /** 34 | * Supplies a PasswordEncoder instance to the Spring ApplicationContext. The 35 | * PasswordEncoder is used by the AuthenticationProvider to perform one-way 36 | * hash operations on passwords for credential comparison. 37 | * 38 | * @return A PasswordEncoder. 39 | */ 40 | @Bean 41 | public PasswordEncoder passwordEncoder() { 42 | return new BCryptPasswordEncoder(); 43 | } 44 | 45 | /** 46 | * This method builds the AuthenticationProvider used by the system to 47 | * process authentication requests. 48 | * 49 | * @param auth An AuthenticationManagerBuilder instance used to construct 50 | * the AuthenticationProvider. 51 | */ 52 | @Autowired 53 | public void configureGlobal(AuthenticationManagerBuilder auth) { 54 | 55 | auth.authenticationProvider(accountAuthenticationProvider); 56 | 57 | } 58 | 59 | /** 60 | * This inner class configures a WebSecurityConfigurerAdapter instance for 61 | * the web service API context paths. 62 | * 63 | * @author Matt Warman 64 | */ 65 | @Configuration 66 | @Order(1) 67 | public static class ApiWebSecurityConfigurerAdapter 68 | extends WebSecurityConfigurerAdapter { 69 | 70 | @Override 71 | protected void configure(HttpSecurity http) throws Exception { 72 | 73 | // @formatter:off 74 | 75 | http 76 | .antMatcher("/api/**") 77 | .authorizeRequests() 78 | .anyRequest().hasRole("USER") 79 | .and() 80 | .httpBasic() 81 | .and() 82 | .sessionManagement() 83 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 84 | .and() 85 | .csrf() 86 | .disable(); 87 | 88 | // @formatter:on 89 | 90 | } 91 | 92 | } 93 | 94 | /** 95 | * This inner class configures a WebSecurityConfigurerAdapter instance for 96 | * the Spring Actuator web service context paths. 97 | * 98 | * @author Matt Warman 99 | */ 100 | @Configuration 101 | @Order(2) 102 | public static class ActuatorWebSecurityConfigurerAdapter 103 | extends WebSecurityConfigurerAdapter { 104 | 105 | @Override 106 | protected void configure(HttpSecurity http) throws Exception { 107 | 108 | // @formatter:off 109 | 110 | http 111 | .antMatcher("/actuators/**") 112 | .authorizeRequests() 113 | .anyRequest().hasRole("SYSADMIN") 114 | .and() 115 | .httpBasic() 116 | .and() 117 | .sessionManagement() 118 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 119 | .and() 120 | .csrf() 121 | .disable(); 122 | 123 | // @formatter:on 124 | 125 | } 126 | 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /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/Account.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import java.util.Set; 4 | 5 | import javax.persistence.CascadeType; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.JoinTable; 10 | import javax.persistence.ManyToMany; 11 | import javax.validation.constraints.NotNull; 12 | 13 | /** 14 | * The Account class is an entity model object. An Account describes the 15 | * security credentials and authentication flags that permit access to 16 | * application functionality. 17 | * 18 | * @author Matt Warman 19 | */ 20 | @Entity 21 | public class Account extends TransactionalEntity { 22 | 23 | private static final long serialVersionUID = 1L; 24 | 25 | @NotNull 26 | private String username; 27 | 28 | @NotNull 29 | private String password; 30 | 31 | @NotNull 32 | private boolean enabled = true; 33 | 34 | @NotNull 35 | private boolean credentialsexpired = false; 36 | 37 | @NotNull 38 | private boolean expired = false; 39 | 40 | @NotNull 41 | private boolean locked = false; 42 | 43 | @ManyToMany( 44 | fetch = FetchType.EAGER, 45 | cascade = CascadeType.ALL) 46 | @JoinTable( 47 | name = "AccountRole", 48 | joinColumns = @JoinColumn( 49 | name = "accountId", 50 | referencedColumnName = "id") , 51 | inverseJoinColumns = @JoinColumn( 52 | name = "roleId", 53 | referencedColumnName = "id") ) 54 | private Set roles; 55 | 56 | public Account() { 57 | 58 | } 59 | 60 | public String getUsername() { 61 | return username; 62 | } 63 | 64 | public void setUsername(String username) { 65 | this.username = username; 66 | } 67 | 68 | public String getPassword() { 69 | return password; 70 | } 71 | 72 | public void setPassword(String password) { 73 | this.password = password; 74 | } 75 | 76 | public boolean isEnabled() { 77 | return enabled; 78 | } 79 | 80 | public void setEnabled(boolean enabled) { 81 | this.enabled = enabled; 82 | } 83 | 84 | public boolean isCredentialsexpired() { 85 | return credentialsexpired; 86 | } 87 | 88 | public void setCredentialsexpired(boolean credentialsexpired) { 89 | this.credentialsexpired = credentialsexpired; 90 | } 91 | 92 | public boolean isExpired() { 93 | return expired; 94 | } 95 | 96 | public void setExpired(boolean expired) { 97 | this.expired = expired; 98 | } 99 | 100 | public boolean isLocked() { 101 | return locked; 102 | } 103 | 104 | public void setLocked(boolean locked) { 105 | this.locked = locked; 106 | } 107 | 108 | public Set getRoles() { 109 | return roles; 110 | } 111 | 112 | public void setRoles(Set roles) { 113 | this.roles = roles; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/model/Greeting.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import javax.persistence.Entity; 4 | 5 | /** 6 | * The Greeting class is an entity model object. 7 | * 8 | * @author Matt Warman 9 | */ 10 | @Entity 11 | public class Greeting extends TransactionalEntity { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | private String text; 16 | 17 | public Greeting() { 18 | 19 | } 20 | 21 | public String getText() { 22 | return text; 23 | } 24 | 25 | public void setText(String text) { 26 | this.text = text; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/model/ReferenceEntity.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Id; 6 | import javax.persistence.MappedSuperclass; 7 | import javax.validation.constraints.NotNull; 8 | 9 | import org.joda.time.DateTime; 10 | 11 | /** 12 | * The parent class for all reference entities (i.e. reference data as opposed 13 | * to transactional data). 14 | * 15 | * @see TransactionalEntity 16 | * 17 | * @author Matt Warman 18 | */ 19 | @MappedSuperclass 20 | public class ReferenceEntity implements Serializable { 21 | 22 | /** 23 | * The default serial version UID. 24 | */ 25 | private static final long serialVersionUID = 1L; 26 | 27 | /** 28 | * The primary key identifier. 29 | */ 30 | @Id 31 | private Long id; 32 | 33 | /** 34 | * The unique code value, sometimes used for external reference. 35 | */ 36 | @NotNull 37 | private String code; 38 | 39 | /** 40 | * A brief description of the entity. 41 | */ 42 | @NotNull 43 | private String label; 44 | 45 | /** 46 | * The ordinal value facilitates sorting the entities. 47 | */ 48 | @NotNull 49 | private Integer ordinal; 50 | 51 | /** 52 | * The timestamp at which the entity's values may be applied or used by the 53 | * system. 54 | */ 55 | @NotNull 56 | private DateTime effectiveAt; 57 | 58 | /** 59 | * The timestamp at which the entity's values cease to be used by the 60 | * system. If null the entity is not expired. 61 | */ 62 | private DateTime expiresAt; 63 | 64 | /** 65 | * The timestamp when this entity instance was created. 66 | */ 67 | @NotNull 68 | private DateTime createdAt; 69 | 70 | public Long getId() { 71 | return id; 72 | } 73 | 74 | public void setId(Long id) { 75 | this.id = id; 76 | } 77 | 78 | public String getCode() { 79 | return code; 80 | } 81 | 82 | public void setCode(String code) { 83 | this.code = code; 84 | } 85 | 86 | public String getLabel() { 87 | return label; 88 | } 89 | 90 | public void setLabel(String label) { 91 | this.label = label; 92 | } 93 | 94 | public Integer getOrdinal() { 95 | return ordinal; 96 | } 97 | 98 | public void setOrdinal(Integer ordinal) { 99 | this.ordinal = ordinal; 100 | } 101 | 102 | public DateTime getEffectiveAt() { 103 | return effectiveAt; 104 | } 105 | 106 | public void setEffectiveAt(DateTime effectiveAt) { 107 | this.effectiveAt = effectiveAt; 108 | } 109 | 110 | public DateTime getExpiresAt() { 111 | return expiresAt; 112 | } 113 | 114 | public void setExpiresAt(DateTime expiresAt) { 115 | this.expiresAt = expiresAt; 116 | } 117 | 118 | public DateTime getCreatedAt() { 119 | return createdAt; 120 | } 121 | 122 | public void setCreatedAt(DateTime createdAt) { 123 | this.createdAt = createdAt; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/model/Role.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import javax.persistence.Entity; 4 | 5 | /** 6 | * The Role class is an entity model object. A Role describes a privilege level 7 | * within the application. A Role is used to authorize an Account to access a 8 | * set of application resources. 9 | * 10 | * @author Matt Warman 11 | */ 12 | @Entity 13 | public class Role extends ReferenceEntity { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | public Role() { 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/model/TransactionalEntity.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.UUID; 5 | 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.Id; 8 | import javax.persistence.MappedSuperclass; 9 | import javax.persistence.PrePersist; 10 | import javax.persistence.PreUpdate; 11 | import javax.persistence.Version; 12 | import javax.validation.constraints.NotNull; 13 | 14 | import org.example.ws.util.RequestContext; 15 | import org.joda.time.DateTime; 16 | 17 | /** 18 | * The parent class for all transactional persistent entities. 19 | * 20 | * @see ReferenceEntity 21 | * 22 | * @author Matt Warman 23 | */ 24 | @MappedSuperclass 25 | public class TransactionalEntity implements Serializable { 26 | 27 | /** 28 | * The default serial version UID. 29 | */ 30 | private static final long serialVersionUID = 1L; 31 | 32 | /** 33 | * The primary key identifier. 34 | */ 35 | @Id 36 | @GeneratedValue 37 | private Long id; 38 | 39 | /** 40 | * A secondary unique identifier which may be used as a reference to this 41 | * entity by external systems. 42 | */ 43 | @NotNull 44 | private String referenceId = UUID.randomUUID().toString(); 45 | 46 | /** 47 | * The entity instance version used for optimistic locking. 48 | */ 49 | @Version 50 | private Integer version; 51 | 52 | /** 53 | * A reference to the entity or process which created this entity instance. 54 | */ 55 | @NotNull 56 | private String createdBy; 57 | 58 | /** 59 | * The timestamp when this entity instance was created. 60 | */ 61 | @NotNull 62 | private DateTime createdAt; 63 | 64 | /** 65 | * A reference to the entity or process which most recently updated this 66 | * entity instance. 67 | */ 68 | private String updatedBy; 69 | 70 | /** 71 | * The timestamp when this entity instance was most recently updated. 72 | */ 73 | private DateTime updatedAt; 74 | 75 | public Long getId() { 76 | return id; 77 | } 78 | 79 | public void setId(Long id) { 80 | this.id = id; 81 | } 82 | 83 | public String getReferenceId() { 84 | return referenceId; 85 | } 86 | 87 | public void setReferenceId(String referenceId) { 88 | this.referenceId = referenceId; 89 | } 90 | 91 | public Integer getVersion() { 92 | return version; 93 | } 94 | 95 | public void setVersion(Integer version) { 96 | this.version = version; 97 | } 98 | 99 | public String getCreatedBy() { 100 | return createdBy; 101 | } 102 | 103 | public void setCreatedBy(String createdBy) { 104 | this.createdBy = createdBy; 105 | } 106 | 107 | public DateTime getCreatedAt() { 108 | return createdAt; 109 | } 110 | 111 | public void setCreatedAt(DateTime createdAt) { 112 | this.createdAt = createdAt; 113 | } 114 | 115 | public String getUpdatedBy() { 116 | return updatedBy; 117 | } 118 | 119 | public void setUpdatedBy(String updatedBy) { 120 | this.updatedBy = updatedBy; 121 | } 122 | 123 | public DateTime getUpdatedAt() { 124 | return updatedAt; 125 | } 126 | 127 | public void setUpdatedAt(DateTime updatedAt) { 128 | this.updatedAt = updatedAt; 129 | } 130 | 131 | /** 132 | * A listener method which is invoked on instances of TransactionalEntity 133 | * (or their subclasses) prior to initial persistence. Sets the 134 | * created audit values for the entity. Attempts to obtain this 135 | * thread's instance of a username from the RequestContext. If none exists, 136 | * throws an IllegalArgumentException. The username is used to set the 137 | * createdBy value. The createdAt value is set to 138 | * the current timestamp. 139 | */ 140 | @PrePersist 141 | public void beforePersist() { 142 | String username = RequestContext.getUsername(); 143 | if (username == null) { 144 | throw new IllegalArgumentException( 145 | "Cannot persist a TransactionalEntity without a username " 146 | + "in the RequestContext for this thread."); 147 | } 148 | setCreatedBy(username); 149 | 150 | setCreatedAt(new DateTime()); 151 | } 152 | 153 | /** 154 | * A listener method which is invoked on instances of TransactionalEntity 155 | * (or their subclasses) prior to being updated. Sets the 156 | * updated audit values for the entity. Attempts to obtain this 157 | * thread's instance of username from the RequestContext. If none exists, 158 | * throws an IllegalArgumentException. The username is used to set the 159 | * updatedBy value. The updatedAt value is set to 160 | * the current timestamp. 161 | */ 162 | @PreUpdate 163 | public void beforeUpdate() { 164 | String username = RequestContext.getUsername(); 165 | if (username == null) { 166 | throw new IllegalArgumentException( 167 | "Cannot update a TransactionalEntity without a username " 168 | + "in the RequestContext for this thread."); 169 | } 170 | setUpdatedBy(username); 171 | 172 | setUpdatedAt(new DateTime()); 173 | } 174 | 175 | /** 176 | * Determines the equality of two TransactionalEntity objects. If the 177 | * supplied object is null, returns false. If both objects are of the same 178 | * class, and their id values are populated and equal, return 179 | * true. Otherwise, return false. 180 | * 181 | * @param that An Object 182 | * @return A boolean 183 | * @see java.lang.Object#equals(java.lang.Object) 184 | */ 185 | public boolean equals(Object that) { 186 | if (that == null) { 187 | return false; 188 | } 189 | if (this.getClass().equals(that.getClass())) { 190 | TransactionalEntity thatTE = (TransactionalEntity) that; 191 | if (this.getId() == null || thatTE.getId() == null) { 192 | return false; 193 | } 194 | if (this.getId().equals(thatTE.getId())) { 195 | return true; 196 | } 197 | } 198 | return false; 199 | } 200 | 201 | /** 202 | * Returns the hash value of this object. 203 | * 204 | * @return An int 205 | * @see java.lang.Object#hashCode() 206 | */ 207 | public int hashCode() { 208 | if (getId() == null) { 209 | return -1; 210 | } 211 | return getId().hashCode(); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.repository; 2 | 3 | import org.example.ws.model.Account; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * The AccountRepository interface is a Spring Data JPA data repository for 9 | * Account entities. The AccountRepository provides all the data access 10 | * behaviors exposed by JpaRepository and additional custom 11 | * behaviors may be defined in this interface. 12 | * 13 | * @author Matt Warman 14 | */ 15 | @Repository 16 | public interface AccountRepository extends JpaRepository { 17 | 18 | /** 19 | * Query for a single Account entity by username. 20 | * 21 | * @param username A String username value to query the repository. 22 | * @return An Account or null if none found. 23 | */ 24 | Account findByUsername(String username); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /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/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.repository; 2 | 3 | import java.util.Collection; 4 | import java.util.Date; 5 | 6 | import org.example.ws.model.Role; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | /** 13 | * The RoleRepository interface is a Spring Data JPA data repository for Role 14 | * entities. The RoleRepository provides all the data access behaviors exposed 15 | * by JpaRepository and additional custom behaviors may be defined 16 | * in this interface. 17 | * 18 | * @author Matt Warman 19 | */ 20 | @Repository 21 | public interface RoleRepository extends JpaRepository { 22 | 23 | /** 24 | * Query for a collection of Role entities by the effectiveAt and expiresAt 25 | * attribute values. Order the collection by the value of the ordinal 26 | * attribute. 27 | * 28 | * Uses the Query Method approach to search the database. 29 | * 30 | * @param effectiveAt A Date effectiveAt attribute value. 31 | * @param expiresAt A Date expiresAt attribute value. 32 | * @return A Collection of Role entity model classes. 33 | */ 34 | Collection findByEffectiveAtBeforeAndExpiresAtAfterOrExpiresAtNullOrderByOrdinalAsc( 35 | Date effectiveAt, Date expiresAt); 36 | 37 | /** 38 | * Query for a collection of Role entities by the effectiveAt and expiresAt 39 | * attribute values. Order the collection by the value of the ordinal 40 | * attribute. 41 | * 42 | * Uses a Query annotated JPQL statement to search the database. 43 | * 44 | * @param effectiveAt A Date effectiveAt attribute value. 45 | * @return A Collection of Role entity model classes. 46 | */ 47 | @Query("SELECT r FROM Role r WHERE r.effectiveAt <= :effectiveAt AND (r.expiresAt IS NULL OR r.expiresAt > :effectiveAt) ORDER BY r.ordinal ASC") 48 | Collection findAllEffective(@Param("effectiveAt") Date effectiveAt); 49 | 50 | /** 51 | * Query for a single Role entity by the code, effectiveAt, and expiresAt 52 | * attribute values. 53 | * 54 | * Uses the Query Method approach to search the database. 55 | * 56 | * @param code A String code attribute value. 57 | * @param effectiveAt A Date effectiveAt attribute value. 58 | * @param expiresAt A Date expiresAt attribute value. 59 | * @return A Role object or null if not found. 60 | */ 61 | Role findByCodeAndEffectiveAtBeforeAndExpiresAtAfterOrExpiresAtNull( 62 | String code, Date effectiveAt, Date expiresAt); 63 | 64 | /** 65 | * Query for a single Role entity by the code, effectiveAt, and expiresAt 66 | * attribute values. 67 | * 68 | * Uses a Query annotated JPQL statement to search the database. 69 | * 70 | * @param code A String code attribute value. 71 | * @param effectiveAt A Date effectiveAt attribute value. 72 | * @return A Role object or null if not found. 73 | */ 74 | @Query("SELECT r FROM Role r WHERE r.code = :code AND r.effectiveAt <= :effectiveAt AND (r.expiresAt IS NULL OR r.expiresAt > :effectiveAt)") 75 | Role findByCodeAndEffective(@Param("code") String code, 76 | @Param("effectiveAt") Date effectiveAt); 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/security/AccountAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.security; 2 | 3 | import org.example.ws.util.RequestContext; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.authentication.BadCredentialsException; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | import org.springframework.stereotype.Component; 14 | 15 | /** 16 | * A Spring Security AuthenticationProvider which extends 17 | * AbstractUserDetailsAuthenticationProvider. This classes uses the 18 | * AccountUserDetailsService to retrieve a UserDetails instance. 19 | * 20 | * A PasswordEncoder compares the supplied authentication credentials to those 21 | * in the UserDetails. 22 | * 23 | * @author Matt Warman 24 | */ 25 | @Component 26 | public class AccountAuthenticationProvider 27 | extends AbstractUserDetailsAuthenticationProvider { 28 | 29 | /** 30 | * The Logger for this class. 31 | */ 32 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 33 | 34 | /** 35 | * A Spring Security UserDetailsService implementation based upon the 36 | * Account entity model. 37 | */ 38 | @Autowired 39 | private AccountUserDetailsService userDetailsService; 40 | 41 | /** 42 | * A PasswordEncoder instance to hash clear test password values. 43 | */ 44 | @Autowired 45 | private PasswordEncoder passwordEncoder; 46 | 47 | @Override 48 | protected void additionalAuthenticationChecks(UserDetails userDetails, 49 | UsernamePasswordAuthenticationToken token) 50 | throws AuthenticationException { 51 | logger.debug("> additionalAuthenticationChecks"); 52 | 53 | if (token.getCredentials() == null 54 | || userDetails.getPassword() == null) { 55 | throw new BadCredentialsException("Credentials may not be null."); 56 | } 57 | 58 | if (!passwordEncoder.matches((String) token.getCredentials(), 59 | userDetails.getPassword())) { 60 | throw new BadCredentialsException("Invalid credentials."); 61 | } 62 | 63 | RequestContext.setUsername(userDetails.getUsername()); 64 | 65 | logger.debug("< additionalAuthenticationChecks"); 66 | } 67 | 68 | @Override 69 | protected UserDetails retrieveUser(String username, 70 | UsernamePasswordAuthenticationToken token) 71 | throws AuthenticationException { 72 | logger.debug("> retrieveUser"); 73 | 74 | UserDetails userDetails = userDetailsService 75 | .loadUserByUsername(username); 76 | 77 | logger.debug("< retrieveUser"); 78 | return userDetails; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/security/AccountUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.security; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | 6 | import org.example.ws.model.Account; 7 | import org.example.ws.model.Role; 8 | import org.example.ws.service.AccountService; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.core.userdetails.User; 15 | import org.springframework.security.core.userdetails.UserDetails; 16 | import org.springframework.security.core.userdetails.UserDetailsService; 17 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 18 | import org.springframework.stereotype.Service; 19 | 20 | /** 21 | * A Spring Security UserDetailsService implementation which creates UserDetails 22 | * objects from the Account and Role entities. 23 | * 24 | * @author Matt Warman 25 | */ 26 | @Service 27 | public class AccountUserDetailsService implements UserDetailsService { 28 | 29 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 30 | 31 | /** 32 | * The AccountService business service. 33 | */ 34 | @Autowired 35 | private AccountService accountService; 36 | 37 | @Override 38 | public UserDetails loadUserByUsername(String username) 39 | throws UsernameNotFoundException { 40 | logger.debug("> loadUserByUsername {}", username); 41 | 42 | Account account = accountService.findByUsername(username); 43 | if (account == null) { 44 | // Not found... 45 | throw new UsernameNotFoundException( 46 | "User " + username + " not found."); 47 | } 48 | 49 | if (account.getRoles() == null || account.getRoles().isEmpty()) { 50 | // No Roles assigned to user... 51 | throw new UsernameNotFoundException("User not authorized."); 52 | } 53 | 54 | Collection grantedAuthorities = new ArrayList(); 55 | for (Role role : account.getRoles()) { 56 | grantedAuthorities.add(new SimpleGrantedAuthority(role.getCode())); 57 | } 58 | 59 | User userDetails = new User(account.getUsername(), 60 | account.getPassword(), account.isEnabled(), 61 | !account.isExpired(), !account.isCredentialsexpired(), 62 | !account.isLocked(), grantedAuthorities); 63 | 64 | logger.debug("< loadUserByUsername {}", username); 65 | return userDetails; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import org.example.ws.model.Account; 4 | 5 | /** 6 | * The AccountService interface defines all public business behaviors for 7 | * operations on the Account entity model and some related entities such as 8 | * Role. 9 | * 10 | * This interface should be injected into AccountService clients, not the 11 | * implementation bean. 12 | * 13 | * @author Matt Warman 14 | */ 15 | public interface AccountService { 16 | 17 | /** 18 | * Find an Account by the username attribute value. 19 | * 20 | * @param username A String username to query the repository. 21 | * @return An Account instance or null if none found. 22 | */ 23 | Account findByUsername(String username); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/service/AccountServiceBean.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.service; 2 | 3 | import org.example.ws.model.Account; 4 | import org.example.ws.repository.AccountRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * The AccountServiceBean encapsulates all business behaviors for operations on 12 | * the Account entity model and some related entities such as Role. 13 | * 14 | * @author Matt Warman 15 | */ 16 | @Service 17 | public class AccountServiceBean implements AccountService { 18 | 19 | /** 20 | * The Logger for this class. 21 | */ 22 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 23 | 24 | /** 25 | * The Spring Data repository for Account entities. 26 | */ 27 | @Autowired 28 | private AccountRepository accountRepository; 29 | 30 | @Override 31 | public Account findByUsername(String username) { 32 | logger.info("> findByUsername"); 33 | Account account = accountRepository.findByUsername(username); 34 | 35 | logger.info("< findByUsername"); 36 | return account; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /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/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/util/RequestContext.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * The RequestContext facilitates the storage of information for the duration of 8 | * a single request (or web service transaction). 9 | * 10 | * RequestContext attributes are stored in ThreadLocal objects. 11 | * 12 | * @author Matt Warman 13 | * 14 | */ 15 | public class RequestContext { 16 | 17 | /** 18 | * The Logger for this class. 19 | */ 20 | private static Logger logger = LoggerFactory 21 | .getLogger(RequestContext.class); 22 | 23 | /** 24 | * ThreadLocal storage of username Strings. 25 | */ 26 | private static ThreadLocal usernames = new ThreadLocal(); 27 | 28 | private RequestContext() { 29 | 30 | } 31 | 32 | /** 33 | * Get the username for the current thread. 34 | * 35 | * @return A String username. 36 | */ 37 | public static String getUsername() { 38 | return usernames.get(); 39 | } 40 | 41 | /** 42 | * Set the username for the current thread. 43 | * 44 | * @param username A String username. 45 | */ 46 | public static void setUsername(String username) { 47 | usernames.set(username); 48 | logger.debug("RequestContext added username {} to current thread", 49 | username); 50 | } 51 | 52 | /** 53 | * Initialize the ThreadLocal attributes for the current thread. 54 | */ 55 | public static void init() { 56 | usernames.set(null); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /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/RoleController.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.api; 2 | 3 | import java.util.Collection; 4 | import java.util.Date; 5 | 6 | import org.example.ws.model.Role; 7 | import org.example.ws.repository.RoleRepository; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | /** 17 | * The RoleController class is a RESTful web service controller. The 18 | * @RestController annotation informs Spring that each 19 | * @RequestMapping method returns a @ResponseBody. 20 | * 21 | * Note: This controller class was created for demonstration and testing 22 | * purposes. Typically, a Repository is not wired directly into a Controller, 23 | * but rather into a Service component which encapsulates the Repository 24 | * behaviors. 25 | * 26 | * @author Matt Warman 27 | */ 28 | @RestController 29 | public class RoleController extends BaseController { 30 | 31 | /** 32 | * The Spring Data JPA Repository for the Role entity model class. 33 | */ 34 | @Autowired 35 | private RoleRepository roleRepository; 36 | 37 | /** 38 | * Web service endpoint to fetch all Role entities. The service returns the 39 | * collection of entities as JSON. 40 | * 41 | * @return A ResponseEntity containing a collection of Role objects. 42 | */ 43 | @RequestMapping( 44 | value = "/api/roles", 45 | method = RequestMethod.GET, 46 | produces = MediaType.APPLICATION_JSON_VALUE) 47 | public ResponseEntity> getRoles() { 48 | 49 | Collection roles = roleRepository.findAllEffective(new Date()); 50 | 51 | return new ResponseEntity>(roles, HttpStatus.OK); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/example/ws/web/filter/RequestContextInitializationFilter.java: -------------------------------------------------------------------------------- 1 | package org.example.ws.web.filter; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.ServletRequest; 8 | import javax.servlet.ServletResponse; 9 | 10 | import org.example.ws.util.RequestContext; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.GenericFilterBean; 17 | 18 | /** 19 | * The RequestContextInitializationFilter is executed for every web request. The 20 | * filter initializes the RequestContext for the current thread, preventing 21 | * leaking of RequestContext attributes from the previous thread's execution. 22 | * 23 | * @author Matt Warman 24 | */ 25 | @Component 26 | @Order(Ordered.HIGHEST_PRECEDENCE) 27 | public class RequestContextInitializationFilter extends GenericFilterBean { 28 | 29 | /** 30 | * The Logger for this class. 31 | */ 32 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 33 | 34 | @Override 35 | public void doFilter(ServletRequest req, ServletResponse resp, 36 | FilterChain chain) throws IOException, ServletException { 37 | logger.info("> doFilter"); 38 | 39 | RequestContext.init(); 40 | 41 | chain.doFilter(req, resp); 42 | logger.info("< doFilter"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /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=600000 11 | batch.greeting.fixeddelay=900000 12 | batch.greeting.initialdelay=300000 13 | batch.greeting.cron=0 */5 * * * * 14 | -------------------------------------------------------------------------------- /src/main/resources/config/application-hsqldb.properties: -------------------------------------------------------------------------------- 1 | ### 2 | # The HSQLDB profile configuration file. 3 | # 4 | # This file is included when the 'hsqldb' Spring profile is active. 5 | ### 6 | 7 | ### 8 | # Data Source Configuration 9 | ### 10 | # Hibernate 11 | spring.jpa.hibernate.ddl-auto=validate 12 | 13 | # Flyway 14 | flyway.locations=classpath:/data/hsqldb/migrations 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/config/application-mysql.properties: -------------------------------------------------------------------------------- 1 | ## 2 | # The MySQL Application Configuration File 3 | # 4 | # This file is included when the 'mysql' Spring Profile is active. 5 | ## 6 | 7 | ## 8 | # Data Source Configuration 9 | ## 10 | # Connection 11 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 12 | spring.datasource.url=jdbc:mysql://localhost/greeting?autoReconnect=true&useSSL=false 13 | spring.datasource.username=greetusr 14 | spring.datasource.password=greetpwd 15 | spring.datasource.name=greeting 16 | 17 | # Pool 18 | spring.datasource.tomcat.max-active=50 19 | spring.datasource.tomcat.initial-size=10 20 | spring.datasource.tomcat.min-idle=5 21 | 22 | spring.datasource.tomcat.test-on-borrow=true 23 | spring.datasource.tomcat.validation-query=select 1; 24 | 25 | spring.datasource.tomcat.time-between-eviction-runs-millis=60000 26 | spring.datasource.tomcat.min-evictable-idle-time-millis=300000 27 | 28 | # Flyway 29 | flyway.locations=classpath:/data/mysql/migrations -------------------------------------------------------------------------------- /src/main/resources/config/application.properties: -------------------------------------------------------------------------------- 1 | ### 2 | # The main application configuration file. 3 | # 4 | # This file is loaded automatically by Spring Boot when the application starts. 5 | ### 6 | 7 | ### 8 | # Profile Configuration 9 | # available profiles: hsqldb, mysql, batch 10 | ### 11 | spring.profiles.active=hsqldb,batch 12 | 13 | ### 14 | # Data Source Configuration 15 | ### 16 | # Hibernate 17 | spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 18 | 19 | # Jadira 20 | spring.jpa.properties.jadira.usertype.autoRegisterUserTypes=true 21 | 22 | # Flyway 23 | flyway.enabled=false 24 | 25 | # Liquibase 26 | liquibase.enabled=true 27 | liquibase.change-log=classpath:/data/changelog/db.changelog-master.xml 28 | 29 | 30 | ### 31 | # Cache Configuration 32 | ### 33 | spring.cache.cache-names=greetings 34 | spring.cache.caffeine.spec=maximumSize=250,expireAfterAccess=600s 35 | 36 | 37 | ### 38 | # Actuator Configuration 39 | ### 40 | endpoints.health.id=status 41 | endpoints.health.sensitive=false 42 | 43 | endpoints.shutdown.enabled=true 44 | endpoints.shutdown.sensitive=false 45 | 46 | management.context-path=/actuators 47 | 48 | management.security.roles=SYSADMIN 49 | 50 | info.app.name=Web Services 51 | info.app.description=A RESTful web services project using Spring Boot. 52 | info.build.group=@project.groupId@ 53 | info.build.artifact=@project.artifactId@ 54 | info.build.version=@project.version@ 55 | 56 | 57 | ### 58 | # Spring Security Configuration 59 | ### 60 | security.user.name=leanstacks 61 | security.user.password=s3cur!T 62 | -------------------------------------------------------------------------------- /src/main/resources/data/changelog/db.changelog-0.0.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'user' and r.code = 'ROLE_USER' 182 | 183 | 184 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'operations' and r.code = 'ROLE_SYSADMIN' 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/main/resources/data/changelog/db.changelog-0.1.0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/data/changelog/db.changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/data/hsqldb/migrations/V0_0_1__initialize.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Engine: HSQLDB 3 | * Version: 0.0.1 4 | * Description: Initial database structure and data. 5 | */ 6 | 7 | /* 8 | * Structure 9 | */ 10 | 11 | CREATE TABLE Greeting ( 12 | id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL, 13 | referenceId VARCHAR(255) NOT NULL, 14 | text VARCHAR(100) NOT NULL, 15 | version INT NOT NULL, 16 | createdBy VARCHAR(100) NOT NULL, 17 | createdAt DATETIME NOT NULL, 18 | updatedBy VARCHAR(100) DEFAULT NULL, 19 | updatedAt DATETIME DEFAULT NULL, 20 | PRIMARY KEY(id), 21 | CONSTRAINT UQ_Greeting_ReferenceId UNIQUE (referenceId) 22 | ); 23 | 24 | CREATE TABLE Account ( 25 | id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL, 26 | referenceId VARCHAR(255) NOT NULL, 27 | username VARCHAR(100) NOT NULL, 28 | password VARCHAR(200) NOT NULL, 29 | enabled BOOLEAN DEFAULT true NOT NULL, 30 | credentialsexpired BOOLEAN DEFAULT false NOT NULL, 31 | expired BOOLEAN DEFAULT false NOT NULL, 32 | locked BOOLEAN DEFAULT false NOT NULL, 33 | version INT NOT NULL, 34 | createdBy VARCHAR(100) NOT NULL, 35 | createdAt DATETIME NOT NULL, 36 | updatedBy VARCHAR(100) DEFAULT NULL, 37 | updatedAt DATETIME DEFAULT NULL, 38 | PRIMARY KEY (id), 39 | CONSTRAINT UQ_Account_ReferenceId UNIQUE (referenceId), 40 | CONSTRAINT UQ_Account_Username UNIQUE (username) 41 | ); 42 | 43 | CREATE TABLE Role ( 44 | id BIGINT NOT NULL, 45 | code VARCHAR(50) NOT NULL, 46 | label VARCHAR(100) NOT NULL, 47 | ordinal INT NOT NULL, 48 | effectiveAt DATETIME NOT NULL, 49 | expiresAt DATETIME DEFAULT NULL, 50 | createdAt DATETIME NOT NULL, 51 | PRIMARY KEY (id), 52 | CONSTRAINT UQ_Role_Code UNIQUE (code) 53 | ); 54 | 55 | CREATE TABLE AccountRole ( 56 | accountId BIGINT NOT NULL, 57 | roleId BIGINT NOT NULL, 58 | PRIMARY KEY (accountId, roleId), 59 | CONSTRAINT FK_AccountRole_AccountId FOREIGN KEY (accountId) REFERENCES Account (id), 60 | CONSTRAINT FK_AccountRole_RoleId FOREIGN KEY (roleId) REFERENCES Role (id) 61 | ); 62 | 63 | /* 64 | * Data 65 | */ 66 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('1e0d5287-67fd-4043-9ac4-b8d358d6d7ce', 'Hello World!', 0, 'user', NOW(), NULL, NULL); 67 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('37c3178d-3b49-47b6-99d1-277b1a3e8df8', 'Hola Mundo!', 0, 'user', NOW(), NULL, NULL); 68 | 69 | 70 | -- password is 'password' 71 | INSERT INTO Account (referenceId, username, password, enabled, credentialsexpired, expired, locked, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('a07bd221-3ecd-4893-a0f0-78d7c0fbf94e', 'user', '$2a$10$9/44Rne7kQqPXa0cY6NfG.3XzScMrCxFYjapoLq/wFmHz7EC9praK', true, false, false, false, 0, 'user', NOW(), NULL, NULL); 72 | -- password is 'operations' 73 | INSERT INTO Account (referenceId, username, password, enabled, credentialsexpired, expired, locked, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('7bd137c8-ab64-4a45-bf2d-d9bae3574622', 'operations', '$2a$10$CoMVfutnv1qZ.fNlHY1Na.rteiJhsDF0jB1o.76qXcfdWN6As27Zm', true, false, false, false, 0, 'user', NOW(), NULL, NULL); 74 | 75 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (1, 'ROLE_USER', 'User', 0, '2015-01-01 00:00:00', NULL, NOW()); 76 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (2, 'ROLE_ADMIN', 'Admin', 1, '2015-01-01 00:00:00', NULL, NOW()); 77 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (3, 'ROLE_SYSADMIN', 'System Admin', 2, '2015-01-01 00:00:00', NULL, NOW()); 78 | 79 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'user' and r.id = 1; 80 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'operations' and r.id = 3; 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/resources/data/hsqldb/migrations/V0_1_0__migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Engine: HSQLDB 3 | * Version: 0.1.0 4 | * Description: 5 | * Database changes for version 0.1.0. 6 | */ 7 | 8 | /* 9 | * Structure 10 | */ 11 | 12 | /* 13 | * Data 14 | */ 15 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('e1707cf6-2aa4-4745-b04f-c9e03dc0a660', 'Howdy', 0, 'user', NOW(), NULL, NULL); 16 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('09aafc85-39d3-40f3-a09d-246a48ee71d1', 'Hi', 0, 'user', NOW(), NULL, NULL); 17 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('40cfa2c0-dd9a-4e94-8768-daeb1a483069', 'Whats Up?', 0, 'user', NOW(), NULL, NULL); 18 | -------------------------------------------------------------------------------- /src/main/resources/data/mysql/migrations/V0_0_1__initialize.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Engine: MySQL 3 | * Version: 0.0.1 4 | * Description: Initial database structure and data. 5 | */ 6 | 7 | /* 8 | * Structure 9 | */ 10 | 11 | CREATE TABLE `Greeting` ( 12 | `id` bigint(20) unsigned NOT NULL auto_increment, 13 | `referenceId` varchar(255) NOT NULL, 14 | `text` varchar(100) NOT NULL, 15 | `version` int(10) unsigned NOT NULL, 16 | `createdBy` varchar(100) NOT NULL, 17 | `createdAt` datetime NOT NULL, 18 | `updatedBy` varchar(100) DEFAULT NULL, 19 | `updatedAt` datetime DEFAULT NULL, 20 | PRIMARY KEY(`id`), 21 | CONSTRAINT `UQ_Greeting_ReferenceId` UNIQUE (`referenceId`) 22 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 23 | 24 | CREATE TABLE `Account` ( 25 | `id` bigint(20) unsigned NOT NULL auto_increment, 26 | `referenceId` varchar(255) NOT NULL, 27 | `username` varchar(100) NOT NULL, 28 | `password` varchar(200) NOT NULL, 29 | `enabled` bit(1) NOT NULL DEFAULT 1, 30 | `credentialsexpired` bit(1) NOT NULL DEFAULT 0, 31 | `expired` bit(1) NOT NULL DEFAULT 0, 32 | `locked` bit(1) NOT NULL DEFAULT 0, 33 | `version` int(10) unsigned NOT NULL, 34 | `createdBy` varchar(100) NOT NULL, 35 | `createdAt` datetime NOT NULL, 36 | `updatedBy` varchar(100) DEFAULT NULL, 37 | `updatedAt` datetime DEFAULT NULL, 38 | PRIMARY KEY (`id`), 39 | CONSTRAINT `UQ_Account_ReferenceId` UNIQUE (`referenceId`), 40 | CONSTRAINT `UQ_Account_Username` UNIQUE (`username`) 41 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 42 | 43 | CREATE TABLE `Role` ( 44 | `id` bigint(20) unsigned NOT NULL, 45 | `code` varchar(50) NOT NULL, 46 | `label` varchar(100) NOT NULL, 47 | `ordinal` int(10) unsigned NOT NULL, 48 | `effectiveAt` datetime NOT NULL, 49 | `expiresAt` datetime DEFAULT NULL, 50 | `createdAt` datetime NOT NULL, 51 | PRIMARY KEY (`id`), 52 | CONSTRAINT `UQ_Role_Code` UNIQUE (`code`) 53 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 54 | 55 | CREATE TABLE `AccountRole` ( 56 | `accountId` bigint(20) unsigned NOT NULL, 57 | `roleId` bigint(20) unsigned NOT NULL, 58 | PRIMARY KEY (`accountId`, `roleId`), 59 | CONSTRAINT `FK_AccountRole_AccountId` FOREIGN KEY (`accountId`) REFERENCES `Account` (`id`) ON DELETE CASCADE, 60 | CONSTRAINT `FK_AccountRole_RoleId` FOREIGN KEY (`roleId`) REFERENCES `Role` (`id`) ON DELETE CASCADE 61 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 62 | 63 | /* 64 | * Data 65 | */ 66 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('1e0d5287-67fd-4043-9ac4-b8d358d6d7ce', 'Hello World!', 0, 'user', NOW(), NULL, NULL); 67 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('37c3178d-3b49-47b6-99d1-277b1a3e8df8', 'Hola Mundo!', 0, 'user', NOW(), NULL, NULL); 68 | 69 | 70 | -- password is 'password' 71 | INSERT INTO Account (referenceId, username, password, enabled, credentialsexpired, expired, locked, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('a07bd221-3ecd-4893-a0f0-78d7c0fbf94e', 'user', '$2a$10$9/44Rne7kQqPXa0cY6NfG.3XzScMrCxFYjapoLq/wFmHz7EC9praK', true, false, false, false, 0, 'user', NOW(), NULL, NULL); 72 | -- password is 'operations' 73 | INSERT INTO Account (referenceId, username, password, enabled, credentialsexpired, expired, locked, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('7bd137c8-ab64-4a45-bf2d-d9bae3574622', 'operations', '$2a$10$CoMVfutnv1qZ.fNlHY1Na.rteiJhsDF0jB1o.76qXcfdWN6As27Zm', true, false, false, false, 0, 'user', NOW(), NULL, NULL); 74 | 75 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (1, 'ROLE_USER', 'User', 0, '2015-01-01 00:00:00', NULL, NOW()); 76 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (2, 'ROLE_ADMIN', 'Admin', 1, '2015-01-01 00:00:00', NULL, NOW()); 77 | INSERT INTO Role (id, code, label, ordinal, effectiveAt, expiresAt, createdAt) VALUES (3, 'ROLE_SYSADMIN', 'System Admin', 2, '2015-01-01 00:00:00', NULL, NOW()); 78 | 79 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'user' and r.id = 1; 80 | INSERT INTO AccountRole (accountId, roleId) SELECT a.id, r.id FROM Account a, Role r WHERE a.username = 'operations' and r.id = 3; 81 | -------------------------------------------------------------------------------- /src/main/resources/data/mysql/migrations/V0_1_0__migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Engine: MySQL 3 | * Version: 0.1.0 4 | * Description: 5 | * Database changes for version 0.1.0. 6 | */ 7 | 8 | /* 9 | * Structure 10 | */ 11 | 12 | /* 13 | * Data 14 | */ 15 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('e1707cf6-2aa4-4745-b04f-c9e03dc0a660', 'Howdy', 0, 'user', NOW(), NULL, NULL); 16 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('09aafc85-39d3-40f3-a09d-246a48ee71d1', 'Hi', 0, 'user', NOW(), NULL, NULL); 17 | INSERT INTO Greeting (referenceId, text, version, createdBy, createdAt, updatedBy, updatedAt) VALUES ('40cfa2c0-dd9a-4e94-8768-daeb1a483069', 'Whats Up?', 0, 'user', NOW(), NULL, NULL); 18 | -------------------------------------------------------------------------------- /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.DeserializationFeature; 15 | import com.fasterxml.jackson.databind.JsonMappingException; 16 | import com.fasterxml.jackson.databind.ObjectMapper; 17 | import com.fasterxml.jackson.datatype.joda.JodaModule; 18 | 19 | /** 20 | * This class extends the functionality of AbstractTest. AbstractControllerTest 21 | * is the parent of all web controller unit test classes. The class ensures that 22 | * a type of WebApplicationContext is built and prepares a MockMvc instance for 23 | * use in test methods. 24 | * 25 | * @author Matt Warman 26 | */ 27 | @WebAppConfiguration 28 | public abstract class AbstractControllerTest extends AbstractTest { 29 | 30 | protected MockMvc mvc; 31 | 32 | @Autowired 33 | protected WebApplicationContext webApplicationContext; 34 | 35 | /** 36 | * Prepares the test class for execution of web tests. Builds a MockMvc 37 | * instance. Call this method from the concrete JUnit test class in the 38 | * @Before setup method. 39 | */ 40 | protected void setUp() { 41 | super.setUp(); 42 | mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 43 | } 44 | 45 | /** 46 | * Prepares the test class for execution of web tests. Builds a MockMvc 47 | * instance using standalone configuration facilitating the injection of 48 | * Mockito resources into the controller class. 49 | * @param controller A controller object to be tested. 50 | */ 51 | protected void setUp(BaseController controller) { 52 | mvc = MockMvcBuilders.standaloneSetup(controller).build(); 53 | } 54 | 55 | /** 56 | * Maps an Object into a JSON String. Uses a Jackson ObjectMapper. 57 | * @param obj The Object to map. 58 | * @return A String of JSON. 59 | * @throws JsonProcessingException Thrown if an error occurs while mapping. 60 | */ 61 | protected String mapToJson(Object obj) throws JsonProcessingException { 62 | ObjectMapper mapper = new ObjectMapper(); 63 | mapper.registerModule(new JodaModule()); 64 | return mapper.writeValueAsString(obj); 65 | } 66 | 67 | /** 68 | * Maps a String of JSON into an instance of a Class of type T. Uses a 69 | * Jackson ObjectMapper. 70 | * @param json A String of JSON. 71 | * @param clazz A Class of type T. The mapper will attempt to convert the 72 | * JSON into an Object of this Class type. 73 | * @return An Object of type T. 74 | * @throws JsonParseException Thrown if an error occurs while mapping. 75 | * @throws JsonMappingException Thrown if an error occurs while mapping. 76 | * @throws IOException Thrown if an error occurs while mapping. 77 | */ 78 | protected T mapFromJson(String json, Class clazz) 79 | throws JsonParseException, JsonMappingException, IOException { 80 | ObjectMapper mapper = new ObjectMapper(); 81 | mapper.registerModule(new JodaModule()); 82 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 83 | false); 84 | return mapper.readValue(json, clazz); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/org/example/ws/AbstractTest.java: -------------------------------------------------------------------------------- 1 | package org.example.ws; 2 | 3 | import org.example.ws.util.RequestContext; 4 | import org.junit.runner.RunWith; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | /** 11 | * The AbstractTest class is the parent of all JUnit test classes. This class 12 | * configures the test ApplicationContext and test runner environment. 13 | * 14 | * @author Matt Warman 15 | */ 16 | @RunWith(SpringRunner.class) 17 | @SpringBootTest( 18 | classes = Application.class) 19 | public abstract class AbstractTest { 20 | 21 | /** 22 | * The Account.username attribute value used by default for unit tests. 23 | */ 24 | public static final String USERNAME = "unittest"; 25 | 26 | /** 27 | * The Logger instance for all classes in the unit test framework. 28 | */ 29 | protected Logger logger = LoggerFactory.getLogger(this.getClass()); 30 | 31 | /** 32 | * Prepares the test class for execution. 33 | */ 34 | protected void setUp() { 35 | 36 | RequestContext.setUsername(AbstractTest.USERNAME); 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /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 | super.setUp(); 31 | service.evictCache(); 32 | } 33 | 34 | @After 35 | public void tearDown() { 36 | // clean up after each test method 37 | } 38 | 39 | @Test 40 | public void testFindAll() { 41 | 42 | Collection list = service.findAll(); 43 | 44 | Assert.assertNotNull("failure - expected not null", list); 45 | Assert.assertEquals("failure - expected list size", 5, list.size()); 46 | 47 | } 48 | 49 | @Test 50 | public void testFindOne() { 51 | 52 | Long id = new Long(1); 53 | 54 | Greeting entity = service.findOne(id); 55 | 56 | Assert.assertNotNull("failure - expected not null", entity); 57 | Assert.assertEquals("failure - expected id attribute match", id, 58 | entity.getId()); 59 | 60 | } 61 | 62 | @Test 63 | public void testFindOneNotFound() { 64 | 65 | Long id = Long.MAX_VALUE; 66 | 67 | Greeting entity = service.findOne(id); 68 | 69 | Assert.assertNull("failure - expected null", entity); 70 | 71 | } 72 | 73 | @Test 74 | public void testCreate() { 75 | 76 | Greeting entity = new Greeting(); 77 | entity.setText("test"); 78 | 79 | Greeting createdEntity = service.create(entity); 80 | 81 | Assert.assertNotNull("failure - expected not null", createdEntity); 82 | Assert.assertNotNull("failure - expected id attribute not null", 83 | createdEntity.getId()); 84 | Assert.assertEquals("failure - expected text attribute match", "test", 85 | createdEntity.getText()); 86 | 87 | Collection list = service.findAll(); 88 | 89 | Assert.assertEquals("failure - expected size", 6, list.size()); 90 | 91 | } 92 | 93 | @Test 94 | public void testCreateWithId() { 95 | 96 | Exception exception = null; 97 | 98 | Greeting entity = new Greeting(); 99 | entity.setId(Long.MAX_VALUE); 100 | entity.setText("test"); 101 | 102 | try { 103 | service.create(entity); 104 | } catch (EntityExistsException e) { 105 | exception = e; 106 | } 107 | 108 | Assert.assertNotNull("failure - expected exception", exception); 109 | Assert.assertTrue("failure - expected EntityExistsException", 110 | exception instanceof EntityExistsException); 111 | 112 | } 113 | 114 | @Test 115 | public void testUpdate() { 116 | 117 | Long id = new Long(1); 118 | 119 | Greeting entity = service.findOne(id); 120 | 121 | Assert.assertNotNull("failure - expected not null", entity); 122 | 123 | String updatedText = entity.getText() + " test"; 124 | entity.setText(updatedText); 125 | Greeting updatedEntity = service.update(entity); 126 | 127 | Assert.assertNotNull("failure - expected not null", updatedEntity); 128 | Assert.assertEquals("failure - expected id attribute match", id, 129 | updatedEntity.getId()); 130 | Assert.assertEquals("failure - expected text attribute match", 131 | updatedText, updatedEntity.getText()); 132 | 133 | } 134 | 135 | @Test 136 | public void testUpdateNotFound() { 137 | 138 | Exception exception = null; 139 | 140 | Greeting entity = new Greeting(); 141 | entity.setId(Long.MAX_VALUE); 142 | entity.setText("test"); 143 | 144 | try { 145 | service.update(entity); 146 | } catch (NoResultException e) { 147 | exception = e; 148 | } 149 | 150 | Assert.assertNotNull("failure - expected exception", exception); 151 | Assert.assertTrue("failure - expected NoResultException", 152 | exception instanceof NoResultException); 153 | 154 | } 155 | 156 | @Test 157 | public void testDelete() { 158 | 159 | Long id = new Long(1); 160 | 161 | Greeting entity = service.findOne(id); 162 | 163 | Assert.assertNotNull("failure - expected not null", entity); 164 | 165 | service.delete(id); 166 | 167 | Collection list = service.findAll(); 168 | 169 | Assert.assertEquals("failure - expected size", 4, list.size()); 170 | 171 | Greeting deletedEntity = service.findOne(id); 172 | 173 | Assert.assertNull("failure - expected null", deletedEntity); 174 | 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------