├── .gitignore ├── LICENSE ├── README.md ├── datetime ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ └── java │ │ └── dev │ │ └── agitrubard │ │ └── datetime │ │ ├── DateTimeApplication.java │ │ ├── controller │ │ └── EventController.java │ │ ├── model │ │ ├── entity │ │ │ └── EventEntity.java │ │ ├── request │ │ │ └── EventCreateRequest.java │ │ └── response │ │ │ └── EventResponse.java │ │ ├── repository │ │ ├── EventRepository.java │ │ └── impl │ │ │ └── EventRepositoryImpl.java │ │ └── service │ │ ├── EventService.java │ │ └── impl │ │ └── EventServiceImpl.java │ └── test │ └── java │ └── dev │ └── agitrubard │ └── datetime │ └── controller │ └── DateTimeEndToEndTests.java ├── dependencyinjection ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ └── java │ └── dev │ └── agitrubard │ └── dependencyinjection │ ├── DependencyInjectionApplication.java │ ├── controller │ └── SomeController.java │ └── service │ ├── SomeService.java │ └── impl │ └── SomeServiceImpl.java ├── guardclause ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ └── java │ └── dev │ └── agitrubard │ └── guardclause │ └── Main.java └── pattern ├── factory ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ └── java │ │ └── dev │ │ └── agitrubard │ │ └── factory │ │ ├── FactoryApplication.java │ │ ├── controller │ │ └── AuthController.java │ │ ├── model │ │ ├── entity │ │ │ └── UserEntity.java │ │ ├── enums │ │ │ └── TwoFactorAuthenticationType.java │ │ └── request │ │ │ ├── LoginRequest.java │ │ │ └── RegisterRequest.java │ │ ├── repository │ │ ├── UserRepository.java │ │ └── impl │ │ │ └── UserRepositoryImpl.java │ │ └── service │ │ ├── LoginService.java │ │ ├── RegisterService.java │ │ ├── TwoFactorAuthenticationService.java │ │ ├── TwoFactorAuthenticationServiceFactory.java │ │ └── impl │ │ ├── EmailAuthenticationServiceImpl.java │ │ ├── LoginServiceImpl.java │ │ ├── PassKeyAuthenticationServiceImpl.java │ │ ├── RegisterServiceImpl.java │ │ ├── SmsAuthenticationServiceImpl.java │ │ └── TwoFactorAuthenticationServiceFactoryImpl.java │ └── test │ └── java │ └── dev │ └── agitrubard │ └── factory │ └── controller │ └── FactoryPatternEndToEndTest.java └── strategy ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── dev │ └── agitrubard │ └── strategy │ ├── StrategyApplication.java │ ├── controller │ └── NotificationController.java │ ├── model │ ├── enums │ │ └── NotificationType.java │ └── request │ │ └── NotificationRequest.java │ └── service │ ├── NotificationService.java │ └── impl │ ├── EmailNotificationServiceImpl.java │ ├── PushNotificationServiceImpl.java │ └── SmsNotificationServiceImpl.java └── test └── java └── dev └── agitrubard └── strategy └── controller └── StrategyPatternEndToEndTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ☕️ 🍃 | Java & Spring Best Practices 2 | 3 | ## 🚀 Introduction 4 | This repository is a collection of best practices and code snippets for Java development using the Spring Framework. Our goal is to help developers use Spring more effectively and write cleaner, more maintainable, and more scalable code. 5 | 6 | ## 📄 Content 7 | In this repository, you will find: 8 | 9 | - **Best Practices:** Guidelines and recommendations for using Spring in various scenarios. 10 | - **Code Snippets:** Short and concise code examples demonstrating the best practices. 11 | - **Explanations:** Clear explanations of the reasoning and best practices behind the code snippets. 12 | 13 | ## 💬 How to Use 14 | - Browse the best practices and identify those that are relevant to your project. 15 | - Copy the corresponding code snippets and paste them into your project. 16 | - Adapt the code snippets to your project's specific requirements if necessary. 17 | - Read the explanations to understand the reasoning and best practices behind the code snippets. 18 | 19 | ## 🧑‍💻 Contributing 20 | You are encouraged to contribute to this repository! You can add new best practices, code snippets, or explanations. To contribute, please submit a pull request using https://docs.github.com/articles/about-pull-requests. 21 | 22 | #### If you have a best practice to share or an improvement to an existing snippet, feel free to open a pull request or an issue. 23 | 24 | 1. Fork the repository 25 | 2. Create a new branch: `git checkout -b feature-branch` 26 | 4. Commit your changes: `git commit -m 'Add some feature'` 27 | 5. Push to the branch: `git push origin feature-branch` 28 | 6. Open a Pull Request to `main` branch 29 | 30 | ## 🙌🏼 Acknowledgements 31 | Thank you to everyone who has contributed to this repository! 32 | 33 | ## **Support** 34 | 35 |

36 | 37 | -------------------------------------------------------------------------------- /datetime/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /datetime/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.1 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 19 | -------------------------------------------------------------------------------- /datetime/README.md: -------------------------------------------------------------------------------- 1 | # Java Spring Best Practices: Handling Date and Time with LocalDate, LocalTime, and LocalDateTime 2 | 3 | This module demonstrates the use of `java.time.LocalDate`, `java.time.LocalTime`, and `java.time.LocalDateTime` in a Spring Boot application for managing events. Additionally, it incorporates a use case that dynamically determines if an event has been published based on the current `LocalDateTime`. 4 | 5 | --- 6 | 7 | ## 📌 Overview 8 | 9 | In this module, we focus on how to handle various date and time representations using Java’s `LocalDate`, `LocalTime`, and `LocalDateTime` classes, and how to implement business logic based on time conditions (e.g., checking if an event has been published). These classes, part of the Java 8 `java.time` package, offer a robust, intuitive, and immutable approach to working with date and time data. 10 | 11 | ### Key Features: 12 | 13 | - **Immutability:** These classes are immutable, which means once they are created, their value cannot be changed. This helps avoid side effects and ensures thread safety in multi-threaded environments. 14 | - **Type Safety:** Using `LocalDate`, `LocalTime`, and `LocalDateTime` ensures that the date and time data is strongly typed, preventing issues with improperly formatted `String` values. 15 | - **Simplified Logic:** Java’s time package provides built-in methods for comparing and manipulating dates and times, making business logic, like checking if an event is published, straightforward. 16 | 17 | --- 18 | 19 | ## 🏛️ **Module Structure** 20 | 21 | ```plaintext 22 | src/ 23 | └── main/ 24 | └── java/ 25 | └── dev/ 26 | └── agitrubard/ 27 | └── datetime/ 28 | ├── controller/ 29 | │ └── EventController.java 30 | ├── model/ 31 | │ ├── entity/ 32 | │ │ └── EventEntity.java 33 | │ ├── request/ 34 | │ │ └── EventCreateRequest.java 35 | │ └── response/ 36 | │ └── EventResponse.java 37 | ├── repository/ 38 | │ └── impl/ 39 | │ └── EventRepositoryImpl.java 40 | └── service/ 41 | └── impl/ 42 | └── EventServiceImpl.java 43 | ``` 44 | 45 | --- 46 | 47 | ## 🚀 **Getting Started** 48 | 49 | ### **Prerequisites** 50 | 51 | - **Java 21** 52 | - **Maven 3.6.x** 53 | - **Spring Boot 3.x.x** 54 | 55 | ### **Installation** 56 | 57 | 1. **Clone the repository:** 58 | 59 | ```bash 60 | git clone https://github.com/agitrubard/java-spring-best-practices.git 61 | cd java-spring-best-practices 62 | ``` 63 | 64 | 2. **Build the project:** 65 | 66 | ```bash 67 | mvn clean install 68 | ``` 69 | 70 | 3. **Run the application:** 71 | 72 | ```bash 73 | mvn spring-boot:run 74 | ``` 75 | 76 | --- 77 | 78 | ## 📄 **Usage** 79 | 80 | ### Creating an Event with Time Logic 81 | 82 | To create an event, make a `POST` request to `/event` with the following JSON payload: 83 | 84 | ```json 85 | { 86 | "name": "Some Event", 87 | "date": "2025-09-23", 88 | "time": "20:00", 89 | "publishAt": "2024-09-23T00:00:00" 90 | } 91 | ``` 92 | 93 | In the backend, the application checks whether the `publishAt` date is in the past, and marks the event as published or unpublished based on this logic. If `publishAt` is `null`, the event is automatically considered published. 94 | 95 | **Example Logic in Event Creation:** 96 | ```java 97 | @Override 98 | public void create(EventCreateRequest createRequest) { 99 | 100 | boolean isPublished = Optional.ofNullable(createRequest.getPublishAt()) 101 | .map(publishAt -> publishAt.isBefore(LocalDateTime.now())) 102 | .orElse(true); 103 | 104 | EventEntity eventEntity = new EventEntity( 105 | createRequest.getName(), 106 | createRequest.getDate(), 107 | createRequest.getTime(), 108 | createRequest.getPublishAt(), 109 | isPublished 110 | ); 111 | 112 | eventRepository.save(eventEntity); 113 | } 114 | ``` 115 | 116 | ### Retrieving Events 117 | 118 | To retrieve all events, send a `GET` request to `/events`: 119 | 120 | ```bash 121 | GET /events 122 | ``` 123 | 124 | Sample Response: 125 | 126 | ```json 127 | [ 128 | { 129 | "id": 1, 130 | "name": "Some Event", 131 | "date": "2025-09-23", 132 | "time": "20:00", 133 | "publishAt": "2024-09-23T00:00:00", 134 | "isPublished": true, 135 | "createdAt": "2024-09-21T09:52:20.12342" 136 | } 137 | ] 138 | ``` 139 | 140 | ### Retrieving an Event by ID 141 | 142 | To retrieve a specific event by ID, use the following endpoint: 143 | 144 | ```bash 145 | GET /event/{id} 146 | ``` 147 | 148 | --- 149 | 150 | ## ⚙️ **Why Use `LocalDate`, `LocalTime`, and `LocalDateTime`?** 151 | 152 | ### **`java.time.LocalDate`** 153 | - Represents a date (year, month, day) without time or time zone. 154 | - Ideal for representing specific days like holidays, deadlines, or event dates. 155 | 156 | ### **`java.time.LocalTime`** 157 | - Represents time (hour, minute, second) without a date or time zone. 158 | - Perfect for representing a specific time of day (e.g., the start time of a meeting). 159 | 160 | ### **`java.time.LocalDateTime`** 161 | - Represents a date and time without time zone information. 162 | - Useful for scenarios where both date and time are needed, like when an event should be published. 163 | 164 | ### **Advantages in a Real-World Application** 165 | 166 | - **Built-in Date Logic**: The `LocalDateTime` object provides methods like `isBefore()` and `isAfter()` that make it easy to perform checks based on the current date and time. 167 | - **Consistency**: Using `LocalDate`, `LocalTime`, and `LocalDateTime` ensures consistency and eliminates the need for manual date formatting or parsing. 168 | - **Simplified Business Logic**: By utilizing these classes, it’s easy to implement logic based on dates, such as automatically marking an event as published if its `publishAt` date is in the past. 169 | 170 | --- 171 | 172 | ### Class Diagram 173 | 174 | ```plaintext 175 | ┌───────────────────────┐ 176 | │ EventController │ 177 | ├───────────────────────┤ 178 | │ + findAll() │ 179 | │ + findById() │ 180 | │ + create() │ 181 | └───────────┬───────────┘ 182 | │ 183 | │ 184 | ┌───────────┴───────────┐ 185 | │ EventService │ 186 | ├───────────────────────┤ 187 | │ + findAll() │ 188 | │ + findById() │ 189 | │ + create() │ 190 | └───────────┬───────────┘ 191 | ┌───────────┴───────────┐ 192 | │ EventServiceImpl │ 193 | ├───────────────────────┤ 194 | │ + findAll() │ 195 | │ + findById() │ 196 | │ + create() │ 197 | └───────────┬───────────┘ 198 | │ 199 | │ 200 | ┌───────────┴───────────┐ 201 | │ EventRepository │ 202 | ├───────────────────────┤ 203 | │ + findAll() │ 204 | │ + findById() │ 205 | │ + save() │ 206 | └───────────┬───────────┘ 207 | ┌───────────┴───────────┐ 208 | │ EventRepositoryImpl │ 209 | ├───────────────────────┤ 210 | │ + findAll() │ 211 | │ + findById() │ 212 | │ + save() │ 213 | └───────────────────────┘ 214 | ``` 215 | 216 | --- 217 | 218 | ## **Conclusion** 219 | 220 | By using `LocalDate`, `LocalTime`, and `LocalDateTime`, we ensure that our application handles date and time information efficiently, and implements logic like determining whether an event is published based on the current time. This simplifies both coding and maintaining business rules around time-based logic. 221 | 222 | **Key Takeaways**: 223 | - No need for custom formatting—Java’s date and time objects provide everything we need out of the box. 224 | - Built-in methods for time comparisons, such as determining whether an event should be published based on `LocalDateTime`. 225 | - Immutability and type safety are maintained, ensuring that your code is secure and easy to understand. 226 | 227 | --- 228 | 229 | This README now highlights the time-based logic you've added while continuing to emphasize the use of `LocalDate`, `LocalTime`, and `LocalDateTime` effectively. Let me know if you need further adjustments! -------------------------------------------------------------------------------- /datetime/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # apply MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /datetime/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /datetime/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.4.6 11 | 12 | 13 | 14 | dev.agitrubard 15 | datetime 16 | 0.0.1-SNAPSHOT 17 | datetime 18 | datetime 19 | 20 | 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-maven-plugin 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/DateTimeApplication.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DateTimeApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DateTimeApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/controller/EventController.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.controller; 2 | 3 | import dev.agitrubard.datetime.model.request.EventCreateRequest; 4 | import dev.agitrubard.datetime.model.response.EventResponse; 5 | import dev.agitrubard.datetime.service.EventService; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | class EventController { 17 | 18 | private final EventService eventService; 19 | 20 | public EventController(EventService eventService) { 21 | this.eventService = eventService; 22 | } 23 | 24 | @GetMapping("/events") 25 | ResponseEntity> findAll() { 26 | List eventResponse = eventService.findAll(); 27 | return ResponseEntity.ok(eventResponse); 28 | } 29 | 30 | @GetMapping("/event/{id}") 31 | ResponseEntity findById(@PathVariable Long id) { 32 | EventResponse eventResponse = eventService.findById(id); 33 | return ResponseEntity.ok(eventResponse); 34 | } 35 | 36 | @PostMapping("/event") 37 | ResponseEntity create(@RequestBody EventCreateRequest createRequest) { 38 | eventService.create(createRequest); 39 | return ResponseEntity.ok().build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/model/entity/EventEntity.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.model.entity; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.time.LocalTime; 6 | 7 | public class EventEntity { 8 | 9 | private Long id; 10 | private String name; 11 | private LocalDate date; 12 | private LocalTime time; 13 | private LocalDateTime publishAt; 14 | private boolean isPublished; 15 | private LocalDateTime createdAt; 16 | 17 | public EventEntity(String name, 18 | LocalDate date, 19 | LocalTime time, 20 | LocalDateTime publishAt, 21 | boolean isPublished) { 22 | 23 | this.name = name; 24 | this.date = date; 25 | this.time = time; 26 | this.publishAt = publishAt; 27 | this.isPublished = isPublished; 28 | } 29 | 30 | public Long getId() { 31 | return id; 32 | } 33 | 34 | public void setId(Long id) { 35 | this.id = id; 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | public void setName(String name) { 43 | this.name = name; 44 | } 45 | 46 | public LocalDate getDate() { 47 | return date; 48 | } 49 | 50 | public void setDate(LocalDate date) { 51 | this.date = date; 52 | } 53 | 54 | public LocalTime getTime() { 55 | return time; 56 | } 57 | 58 | public void setTime(LocalTime time) { 59 | this.time = time; 60 | } 61 | 62 | public LocalDateTime getPublishAt() { 63 | return publishAt; 64 | } 65 | 66 | public void setPublishAt(LocalDateTime publishAt) { 67 | this.publishAt = publishAt; 68 | } 69 | 70 | public boolean isPublished() { 71 | return isPublished; 72 | } 73 | 74 | public void setPublished(boolean published) { 75 | isPublished = published; 76 | } 77 | 78 | public LocalDateTime getCreatedAt() { 79 | return createdAt; 80 | } 81 | 82 | public void setCreatedAt(LocalDateTime createdAt) { 83 | this.createdAt = createdAt; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/model/request/EventCreateRequest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.model.request; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.time.LocalTime; 6 | 7 | public class EventCreateRequest { 8 | 9 | private String name; 10 | private LocalDate date; 11 | private LocalTime time; 12 | private LocalDateTime publishAt; 13 | 14 | public EventCreateRequest() { 15 | } 16 | 17 | public EventCreateRequest(String name, LocalDate date, LocalTime time, LocalDateTime publishAt) { 18 | this.name = name; 19 | this.date = date; 20 | this.time = time; 21 | this.publishAt = publishAt; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public LocalDate getDate() { 29 | return date; 30 | } 31 | 32 | public LocalTime getTime() { 33 | return time; 34 | } 35 | 36 | public LocalDateTime getPublishAt() { 37 | return publishAt; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | 43 | if (publishAt == null) { 44 | return "{" + 45 | "\"name\": \"" + name + "\"," + 46 | "\"date\": \"" + date + "\"," + 47 | "\"time\": \"" + time + "\"" + 48 | "}"; 49 | } 50 | 51 | return "{" + 52 | "\"name\": \"" + name + "\"," + 53 | "\"date\": \"" + date + "\"," + 54 | "\"time\": \"" + time + "\"," + 55 | "\"publishAt\": \"" + publishAt + "\"" + 56 | "}"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/model/response/EventResponse.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.model.response; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.time.LocalTime; 6 | 7 | public class EventResponse { 8 | 9 | private Long id; 10 | private String name; 11 | private LocalDate date; 12 | private LocalTime time; 13 | private LocalDateTime publishAt; 14 | private boolean isPublished; 15 | private LocalDateTime createdAt; 16 | 17 | public EventResponse(Long id, 18 | String name, 19 | LocalDate date, 20 | LocalTime time, 21 | LocalDateTime publishAt, 22 | boolean isPublished, 23 | LocalDateTime createdAt) { 24 | 25 | this.id = id; 26 | this.name = name; 27 | this.date = date; 28 | this.time = time; 29 | this.publishAt = publishAt; 30 | this.isPublished = isPublished; 31 | this.createdAt = createdAt; 32 | } 33 | 34 | 35 | public Long getId() { 36 | return id; 37 | } 38 | 39 | public void setId(Long id) { 40 | this.id = id; 41 | } 42 | 43 | public String getName() { 44 | return name; 45 | } 46 | 47 | public void setName(String name) { 48 | this.name = name; 49 | } 50 | 51 | public LocalDate getDate() { 52 | return date; 53 | } 54 | 55 | public void setDate(LocalDate date) { 56 | this.date = date; 57 | } 58 | 59 | public LocalTime getTime() { 60 | return time; 61 | } 62 | 63 | public void setTime(LocalTime time) { 64 | this.time = time; 65 | } 66 | 67 | public LocalDateTime getPublishAt() { 68 | return publishAt; 69 | } 70 | 71 | public void setPublishAt(LocalDateTime publishAt) { 72 | this.publishAt = publishAt; 73 | } 74 | 75 | public boolean isPublished() { 76 | return isPublished; 77 | } 78 | 79 | public void setPublished(boolean published) { 80 | isPublished = published; 81 | } 82 | 83 | public LocalDateTime getCreatedAt() { 84 | return createdAt; 85 | } 86 | 87 | public void setCreatedAt(LocalDateTime createdAt) { 88 | this.createdAt = createdAt; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/repository/EventRepository.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.repository; 2 | 3 | import dev.agitrubard.datetime.model.entity.EventEntity; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface EventRepository { 9 | 10 | List findAll(); 11 | 12 | Optional findById(Long id); 13 | 14 | void save(EventEntity eventEntity); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/repository/impl/EventRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.repository.impl; 2 | 3 | import dev.agitrubard.datetime.model.entity.EventEntity; 4 | import dev.agitrubard.datetime.repository.EventRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | @Repository 13 | class EventRepositoryImpl implements EventRepository { 14 | 15 | private final List events = new ArrayList<>(); 16 | 17 | public List findAll() { 18 | return events; 19 | } 20 | 21 | public Optional findById(Long id) { 22 | return events.stream() 23 | .filter(eventEntity -> eventEntity.getId().equals(id)) 24 | .findFirst(); 25 | } 26 | 27 | public void save(EventEntity eventEntity) { 28 | eventEntity.setId(events.size() + 1L); 29 | eventEntity.setCreatedAt(LocalDateTime.now()); 30 | events.add(eventEntity); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/service/EventService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.service; 2 | 3 | import dev.agitrubard.datetime.model.request.EventCreateRequest; 4 | import dev.agitrubard.datetime.model.response.EventResponse; 5 | 6 | import java.util.List; 7 | 8 | public interface EventService { 9 | 10 | List findAll(); 11 | 12 | EventResponse findById(Long id); 13 | 14 | void create(EventCreateRequest createRequest); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /datetime/src/main/java/dev/agitrubard/datetime/service/impl/EventServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.service.impl; 2 | 3 | import dev.agitrubard.datetime.model.entity.EventEntity; 4 | import dev.agitrubard.datetime.model.request.EventCreateRequest; 5 | import dev.agitrubard.datetime.model.response.EventResponse; 6 | import dev.agitrubard.datetime.repository.EventRepository; 7 | import dev.agitrubard.datetime.service.EventService; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | class EventServiceImpl implements EventService { 17 | 18 | private final EventRepository eventRepository; 19 | 20 | public EventServiceImpl(EventRepository eventRepository) { 21 | this.eventRepository = eventRepository; 22 | } 23 | 24 | @Override 25 | public List findAll() { 26 | List eventEntities = eventRepository.findAll(); 27 | return eventEntities.stream() 28 | .map(eventEntity -> new EventResponse( 29 | eventEntity.getId(), 30 | eventEntity.getName(), 31 | eventEntity.getDate(), 32 | eventEntity.getTime(), 33 | eventEntity.getPublishAt(), 34 | eventEntity.isPublished(), 35 | eventEntity.getCreatedAt() 36 | ) 37 | ) 38 | .collect(Collectors.toList()); 39 | } 40 | 41 | @Override 42 | public EventResponse findById(Long id) { 43 | 44 | EventEntity eventEntity = eventRepository.findById(id) 45 | .orElseThrow(() -> new RuntimeException("Event not found")); 46 | 47 | return new EventResponse( 48 | eventEntity.getId(), 49 | eventEntity.getName(), 50 | eventEntity.getDate(), 51 | eventEntity.getTime(), 52 | eventEntity.getPublishAt(), 53 | eventEntity.isPublished(), 54 | eventEntity.getCreatedAt() 55 | ); 56 | } 57 | 58 | @Override 59 | public void create(EventCreateRequest createRequest) { 60 | 61 | boolean isPublished = Optional.ofNullable(createRequest.getPublishAt()) 62 | .map(publishAt -> publishAt.isBefore(LocalDateTime.now())) 63 | .orElse(true); 64 | 65 | EventEntity eventEntity = new EventEntity( 66 | createRequest.getName(), 67 | createRequest.getDate(), 68 | createRequest.getTime(), 69 | createRequest.getPublishAt(), 70 | isPublished 71 | ); 72 | 73 | eventRepository.save(eventEntity); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /datetime/src/test/java/dev/agitrubard/datetime/controller/DateTimeEndToEndTests.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.datetime.controller; 2 | 3 | import dev.agitrubard.datetime.model.entity.EventEntity; 4 | import dev.agitrubard.datetime.repository.EventRepository; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 15 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 16 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 17 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 18 | 19 | import java.util.List; 20 | 21 | @SpringBootTest 22 | @AutoConfigureMockMvc 23 | @ExtendWith(MockitoExtension.class) 24 | class DateTimeEndToEndTests { 25 | 26 | @Autowired 27 | private MockMvc mockMvc; 28 | 29 | @Autowired 30 | private EventRepository eventRepository; 31 | 32 | @Test 33 | void givenEventCreateRequestWithPastPublishAt_whenEventCreatedWithIsPublishedTrue_thenReturnOk() throws Exception { 34 | // Given 35 | String eventCreateRequest = """ 36 | { 37 | "name": "Event 1", 38 | "date": "2025-09-21", 39 | "time": "10:00", 40 | "publishAt": "2021-01-01T09:00" 41 | } 42 | """; 43 | 44 | // Then 45 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 46 | .post("/event") 47 | .content(eventCreateRequest) 48 | .contentType(MediaType.APPLICATION_JSON); 49 | 50 | mockMvc.perform(mockHttpServletRequestBuilder) 51 | .andDo(MockMvcResultHandlers.print()) 52 | .andExpect(MockMvcResultMatchers.status().isOk()); 53 | 54 | // Verify 55 | List eventEntities = eventRepository.findAll(); 56 | 57 | EventEntity lastEventEntity = eventEntities.getLast(); 58 | Assertions.assertEquals("Event 1", lastEventEntity.getName()); 59 | Assertions.assertEquals("2025-09-21", lastEventEntity.getDate().toString()); 60 | Assertions.assertEquals("10:00", lastEventEntity.getTime().toString()); 61 | Assertions.assertEquals("2021-01-01T09:00", lastEventEntity.getPublishAt().toString()); 62 | 63 | Assertions.assertTrue(lastEventEntity.isPublished()); 64 | Assertions.assertNotNull(lastEventEntity.getCreatedAt()); 65 | } 66 | 67 | @Test 68 | void givenEventCreateRequestWithoutPublishAt_whenEventCreatedWithIsPublishedTrue_thenReturnOk() throws Exception { 69 | // Given 70 | String eventCreateRequest = """ 71 | { 72 | "name": "Event 1", 73 | "date": "2025-09-21", 74 | "time": "10:00" 75 | } 76 | """; 77 | 78 | // Then 79 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 80 | .post("/event") 81 | .content(eventCreateRequest) 82 | .contentType(MediaType.APPLICATION_JSON); 83 | 84 | mockMvc.perform(mockHttpServletRequestBuilder) 85 | .andDo(MockMvcResultHandlers.print()) 86 | .andExpect(MockMvcResultMatchers.status().isOk()); 87 | 88 | // Verify 89 | List eventEntities = eventRepository.findAll(); 90 | 91 | EventEntity lastEventEntity = eventEntities.getLast(); 92 | Assertions.assertEquals("Event 1", lastEventEntity.getName()); 93 | Assertions.assertEquals("2025-09-21", lastEventEntity.getDate().toString()); 94 | Assertions.assertEquals("10:00", lastEventEntity.getTime().toString()); 95 | Assertions.assertNull(lastEventEntity.getPublishAt()); 96 | 97 | Assertions.assertTrue(lastEventEntity.isPublished()); 98 | Assertions.assertNotNull(lastEventEntity.getCreatedAt()); 99 | } 100 | 101 | @Test 102 | void givenEventCreateRequestWithFuturePublishAt_whenEventCreatedWithIsPublishedFalse_thenReturnOk() throws Exception { 103 | // Given 104 | String eventCreateRequest = """ 105 | { 106 | "name": "Event 1", 107 | "date": "3080-09-21", 108 | "time": "10:00", 109 | "publishAt": "3079-10-21T00:00" 110 | } 111 | """; 112 | 113 | // Then 114 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 115 | .post("/event") 116 | .content(eventCreateRequest) 117 | .contentType(MediaType.APPLICATION_JSON); 118 | 119 | mockMvc.perform(mockHttpServletRequestBuilder) 120 | .andDo(MockMvcResultHandlers.print()) 121 | .andExpect(MockMvcResultMatchers.status().isOk()); 122 | 123 | // Verify 124 | List eventEntities = eventRepository.findAll(); 125 | 126 | EventEntity lastEventEntity = eventEntities.getLast(); 127 | Assertions.assertEquals("Event 1", lastEventEntity.getName()); 128 | Assertions.assertEquals("3080-09-21", lastEventEntity.getDate().toString()); 129 | Assertions.assertEquals("10:00", lastEventEntity.getTime().toString()); 130 | Assertions.assertEquals("3079-10-21T00:00", lastEventEntity.getPublishAt().toString()); 131 | 132 | Assertions.assertFalse(lastEventEntity.isPublished()); 133 | Assertions.assertNotNull(lastEventEntity.getCreatedAt()); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /dependencyinjection/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /dependencyinjection/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.1 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 19 | -------------------------------------------------------------------------------- /dependencyinjection/README.md: -------------------------------------------------------------------------------- 1 | # Java Spring Best Practices: Dependency Injection Module 2 | 3 | This module demonstrates the best practices for using Dependency Injection in a Spring Boot application. 4 | 5 | --- 6 | 7 | ## 📌 Overview 8 | 9 | This module illustrates how to effectively use Dependency Injection in a Spring Boot application. Dependency Injection ( 10 | DI) is a fundamental principle in Spring that promotes loose coupling and enhances testability by injecting dependencies 11 | rather than instantiating them directly within the classes. 12 | 13 | ### Key Features: 14 | 15 | - **Constructor Injection:** Preferred method for injecting dependencies, ensuring immutability and making the code 16 | easier to test. 17 | - **Separation of Concerns:** Keeps your business logic separate from dependency management. 18 | - **Clean and Maintainable Code:** Promotes cleaner code architecture by adhering to SOLID principles. 19 | 20 | --- 21 | 22 | ## 🏛️ Module Structure 23 | 24 | ``` 25 | src/ 26 | ├── main/ 27 | │ ├── java/ 28 | │ │ └── dev/ 29 | │ │ └── agitrubard/ 30 | │ │ └── dependencyinjection/ 31 | │ │ ├── controller/ 32 | │ │ │ └── SomeController.java 33 | │ │ └── service/ 34 | │ │ ├── SomeService.java 35 | │ │ └── impl/ 36 | │ │ └── SomeServiceImpl.java 37 | │ └── resources/ 38 | │ └── application.properties 39 | └── test/ 40 | └── java/ 41 | └── dev/ 42 | └── agitrubard/ 43 | └── dependencyinjection/ 44 | └── SomeControllerTests.java 45 | ``` 46 | 47 | --- 48 | 49 | ## 🚀 Getting Started 50 | 51 | ### Prerequisites 52 | 53 | - **Java 21** 54 | - **Maven 3.x.x** 55 | - **Spring Boot 3.x.x** 56 | 57 | ### Installation 58 | 59 | **1. Clone the repository:** 60 | 61 | ```bash 62 | git clone https://github.com/agitrubard/java-spring-best-practices.git 63 | cd java-spring-best-practices 64 | ``` 65 | 66 | **2. Build the project:** 67 | 68 | ```bash 69 | mvn clean install 70 | ``` 71 | 72 | **3. Run the application:** 73 | 74 | ```bash 75 | mvn spring-boot:run 76 | ``` 77 | 78 | --- 79 | 80 | ## 📄 Usage 81 | 82 | ### Asking Something 83 | 84 | To ask something, make a `GET` request to `/something/ask`. This endpoint will use the SomeService to respond with a 85 | friendly message. 86 | 87 | #### Example Response 88 | 89 | ```json 90 | "Slm, nbr?" 91 | ``` 92 | 93 | --- 94 | 95 | ### Controller 96 | 97 | `SomeController` handles the HTTP requests. It uses constructor injection to receive an instance of `SomeService`. 98 | 99 | ```java 100 | @RestController 101 | @RequestMapping("/something") 102 | class SomeController { 103 | 104 | private final SomeService someService; 105 | 106 | public SomeController(SomeService someService) { 107 | this.someService = someService; 108 | } 109 | 110 | @GetMapping("/ask") 111 | ResponseEntity askSomething() { 112 | String something = someService.askSomething(); 113 | return ResponseEntity.ok(something); 114 | } 115 | 116 | } 117 | ``` 118 | 119 | ### Service 120 | 121 | `SomeService` is an interface that defines the contract for asking something. 122 | 123 | ```java 124 | public interface SomeService { 125 | 126 | String askSomething(); 127 | 128 | } 129 | ``` 130 | 131 | ### Service Implementation 132 | 133 | `SomeServiceImpl` provides the actual implementation of `SomeService`. 134 | 135 | ```java 136 | 137 | @Service 138 | class SomeServiceImpl implements SomeService { 139 | 140 | @Override 141 | public String askSomething() { 142 | return "Slm, nbr?"; 143 | } 144 | 145 | } 146 | ``` 147 | 148 | --- 149 | 150 | ## 📙 Dependency Injection Best Practices 151 | 152 | - **Use Constructor Injection:** Preferred over field or setter injection as it promotes immutability and ensures that 153 | the dependency is provided when the object is created. 154 | - **Avoid Circular Dependencies:** Design your services to avoid circular references, which can lead to complicated 155 | dependency graphs and potential application startup issues. 156 | - **Use Interfaces:** Inject interfaces rather than concrete implementations to adhere to the Dependency Inversion 157 | Principle and make your code more flexible and testable. 158 | - **Promote Single Responsibility:** Each class should have one responsibility. For example, the controller handles HTTP 159 | requests, and the service handles business logic. 160 | 161 | ### Class Diagram 162 | 163 | ```plaintext 164 | ┌──────────────────────┐ 165 | │ SomeController │ 166 | ├──────────────────────┤ 167 | │ - someService │ 168 | └───────────┬──────────┘ 169 | │ 170 | │ 171 | ┌───────────┴──────────────┐ 172 | │ SomeService │ 173 | ├──────────────────────────┤ 174 | │ + askSomething(): String │ 175 | └───────────┬──────────────┘ 176 | │ 177 | │ 178 | ┌───────────┴──────────────┐ 179 | │ SomeServiceImpl │ 180 | ├──────────────────────────┤ 181 | │ + askSomething(): String │ 182 | └──────────────────────────┘ 183 | ``` 184 | 185 | In this diagram: 186 | 187 | - `SomeController` depends on `SomeService`. 188 | - `SomeServiceImpl` implements `SomeService`. -------------------------------------------------------------------------------- /dependencyinjection/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # apply MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /dependencyinjection/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /dependencyinjection/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.4.6 11 | 12 | 13 | 14 | dev.agitrubard 15 | dependencyinjection 16 | 0.0.1-SNAPSHOT 17 | dependencyinjection 18 | dependencyinjection 19 | 20 | 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-maven-plugin 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dependencyinjection/src/main/java/dev/agitrubard/dependencyinjection/DependencyInjectionApplication.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.dependencyinjection; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DependencyInjectionApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DependencyInjectionApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /dependencyinjection/src/main/java/dev/agitrubard/dependencyinjection/controller/SomeController.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.dependencyinjection.controller; 2 | 3 | import dev.agitrubard.dependencyinjection.service.SomeService; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/something") 11 | class SomeController { 12 | 13 | /** 14 | * The service that will be injected into this controller. 15 | *

16 | * Best practice is to use constructor injection. 17 | */ 18 | private final SomeService someService; 19 | 20 | public SomeController(SomeService someService) { 21 | this.someService = someService; 22 | } 23 | 24 | @GetMapping("/ask") 25 | ResponseEntity askSomething() { 26 | String something = someService.askSomething(); 27 | return ResponseEntity.ok(something); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /dependencyinjection/src/main/java/dev/agitrubard/dependencyinjection/service/SomeService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.dependencyinjection.service; 2 | 3 | public interface SomeService { 4 | 5 | String askSomething(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /dependencyinjection/src/main/java/dev/agitrubard/dependencyinjection/service/impl/SomeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.dependencyinjection.service.impl; 2 | 3 | import dev.agitrubard.dependencyinjection.service.SomeService; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | class SomeServiceImpl implements SomeService { 8 | 9 | @Override 10 | public String askSomething() { 11 | return "Slm, nbr?"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /guardclause/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /guardclause/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.1 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 19 | -------------------------------------------------------------------------------- /guardclause/README.md: -------------------------------------------------------------------------------- 1 | # Java Best Practices: Guard Clauses 🆚 Nested If-Else Statements 2 | 3 | This module demonstrates the difference between using **Guard Clauses** and **Nested If-Else Statements** for validating 4 | user input in Java. It highlights the advantages of guard clauses in improving code readability, maintainability, and 5 | reducing complexity compared to nested conditional logic. 6 | 7 | --- 8 | 9 | ## 📌 Overview 10 | 11 | Validation is a critical part of any application to ensure the correctness of input data. Two common approaches for 12 | implementing validation are: 13 | 14 | - **Guard Clauses**: Early exit conditions to handle invalid input. 15 | - **Nested If-Else Statements**: Layered conditions to check and handle input step by step. 16 | 17 | This module compares the two methods in terms of: 18 | 19 | - **Readability** 20 | - **Maintainability** 21 | - **Complexity** 22 | 23 | Both approaches validate the same input criteria: 24 | 25 | 1. **Username** must not be `null` or empty. 26 | 2. **Age** must not be `null` and must be greater than or equal to 18. 27 | 3. **Roles** must not be `null` or empty. 28 | 29 | --- 30 | 31 | ## 🚀 Key Features 32 | 33 | ### **Guard Clauses** 34 | 35 | - **Readability:** Each condition is handled independently, with immediate feedback for invalid inputs. 36 | - **Maintainability:** Adding, removing, or updating validation rules is straightforward. 37 | - **Reduced Complexity:** Avoids deeply nested structures, making the code easier to follow. 38 | - **Failure Early:** Stops processing as soon as an invalid condition is found. 39 | 40 | ### **Nested If-Else Statements** 41 | 42 | - **Poor Readability:** Deeply nested structures make the code harder to scan and understand. 43 | - **Difficult Maintenance:** Adding or updating validation rules requires careful restructuring. 44 | - **High Complexity:** Increases the cognitive load on developers to understand the logic flow. 45 | - **Unnecessary Indentation:** Excessive nesting reduces code clarity. 46 | 47 | --- 48 | 49 | ## 🛠️ Example Code 50 | 51 | ### **Guard Clauses Implementation** 52 | 53 | ```java 54 | public static void processUserWithClauseGuard(String username, Integer age, List roles) { 55 | 56 | if (username == null) { 57 | throw new RuntimeException("Username cannot be null"); 58 | } 59 | 60 | if (username.isEmpty()) { 61 | throw new RuntimeException("Username cannot be empty"); 62 | } 63 | 64 | if (age == null) { 65 | throw new RuntimeException("Age cannot be null"); 66 | } 67 | 68 | if (age < 18) { 69 | throw new RuntimeException("Age must be greater than 18"); 70 | } 71 | 72 | if (roles == null || roles.isEmpty()) { 73 | throw new RuntimeException("Roles cannot be empty"); 74 | } 75 | 76 | System.out.println("Process user with clause guard..."); 77 | } 78 | ``` 79 | 80 | ### **Nested If-Else Statements Implementation** 81 | 82 | ```java 83 | public static void processUserWithNestedIfElse(String username, Integer age, List roles) { 84 | 85 | if (username != null) { 86 | if (!username.isEmpty()) { 87 | if (age != null) { 88 | if (age >= 18) { 89 | if (roles != null && !roles.isEmpty()) { 90 | System.out.println("Process user with nested if-else..."); 91 | } else { 92 | throw new RuntimeException("Roles cannot be empty"); 93 | } 94 | } else { 95 | throw new RuntimeException("Age must be greater than 18"); 96 | } 97 | } else { 98 | throw new RuntimeException("Age cannot be null"); 99 | } 100 | } else { 101 | throw new RuntimeException("Username cannot be empty"); 102 | } 103 | } else { 104 | throw new RuntimeException("Username cannot be null"); 105 | } 106 | } 107 | ``` 108 | 109 | --- 110 | 111 | ### 📄 Comparison: Guard Clauses vs. Nested If-Else 112 | 113 | | Feature | Guard Clauses | Nested If-Else | 114 | |---------------------|------------------------------------|-------------------------------------------| 115 | | **Readability** | Clear, concise, and easy to follow | Hard to read due to deep nesting | 116 | | **Maintainability** | Easy to modify | Difficult to update or restructure | 117 | | **Complexity** | Low | High, especially with multiple conditions | 118 | | **Error Handling** | Immediate | Delayed due to layered conditions | 119 | | **Indentation** | Minimal | Excessive | 120 | 121 | --- 122 | 123 | ### 🏁 Conclusion 124 | 125 | For most scenarios, **Guard Clauses** are the recommended approach due to their: 126 | 127 | - **Simplicity:** Early exit for invalid conditions avoids unnecessary complexity. 128 | - **Clarity:** Each condition is isolated, making the code easier to read and understand. 129 | - **Maintainability:** New rules can be added or existing ones updated without impacting unrelated logic. 130 | 131 | While **Nested If-Else Statements** can work for simple cases, they quickly become unmanageable as conditions grow in 132 | number and complexity. Opting for guard clauses leads to cleaner, more maintainable, and more professional code. 133 | 134 | --- 135 | 136 | Feel free to extend this module with your own examples or additional comparisons! Let me know if you have any feedback 137 | or questions. 138 | -------------------------------------------------------------------------------- /guardclause/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # apply MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /guardclause/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /guardclause/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | dev.agitrubard 8 | guardclause 9 | 0.0.1-SNAPSHOT 10 | guardclause 11 | guardclause 12 | 13 | 14 | 21 15 | 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | ${java.version} 24 | ${java.version} 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /guardclause/src/main/java/dev/agitrubard/guardclause/Main.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.guardclause; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * A class demonstrating the difference between using guard clauses and nested if-else statements 7 | * for validating user input in Java. 8 | * 9 | *

Purpose

10 | * This class provides two methods: 11 | * 15 | * Both methods achieve the same functionality but differ significantly in terms of readability, maintainability, and 16 | * overall code quality. 17 | * 18 | *

Comparison: Guard Clauses vs. Nested If-Else

19 | * Guard Clauses: 20 | * 30 | * 31 | * Nested If-Else Statements: 32 | * 41 | * 42 | *

Recommendation

43 | *

44 | * For most scenarios, using guard clauses is the preferred approach due to its simplicity, clarity, and ease of maintenance. 45 | * While nested if-else statements can work for small or simple logic, they quickly become unmanageable as the number of 46 | * conditions increases. 47 | *

48 | */ 49 | public class Main { 50 | 51 | public static void main(String[] args) { 52 | String username = "agitrubard"; 53 | Integer age = 25; 54 | List roles = List.of("admin"); 55 | 56 | processUserWithGuardClause(username, age, roles); 57 | 58 | processUserWithNestedIfElse(username, age, roles); 59 | } 60 | 61 | 62 | /** 63 | * Processes user input using guard clauses to validate input parameters. 64 | * 65 | *

This method uses a series of guard clauses to ensure that the input parameters 66 | * meet the required criteria. If any condition is violated, a {@link RuntimeException} 67 | * is thrown immediately, avoiding deeply nested logic and improving code readability.

68 | * 69 | * Validation rules: 70 | * 75 | * 76 | * @param username the username of the user; must not be null or empty. 77 | * @param age the age of the user; must not be null and must be greater than or equal to 18. 78 | * @param roles the list of roles assigned to the user; must not be null or empty. 79 | * @throws RuntimeException if any of the validation rules are violated. 80 | */ 81 | public static void processUserWithGuardClause(String username, Integer age, List roles) { 82 | 83 | if (username == null) { 84 | throw new RuntimeException("Username cannot be null"); 85 | } 86 | 87 | if (username.isEmpty()) { 88 | throw new RuntimeException("Username cannot be empty"); 89 | } 90 | 91 | if (age == null) { 92 | throw new RuntimeException("Age cannot be null"); 93 | } 94 | 95 | if (age < 18) { 96 | throw new RuntimeException("Age must be greater than 18"); 97 | } 98 | 99 | if (roles == null || roles.isEmpty()) { 100 | throw new RuntimeException("Roles cannot be empty"); 101 | } 102 | 103 | System.out.println("Process user with clause guard..."); 104 | } 105 | 106 | 107 | /** 108 | * Processes user input using nested if-else statements to validate input parameters. 109 | * 110 | *

This method uses a series of nested if-else statements to validate the input parameters. 111 | * While functionally equivalent to the guard clause method, this approach results in deeply nested 112 | * logic, which can make the code harder to read and maintain.

113 | * 114 | * Validation rules: 115 | * 120 | * 121 | * @param username the username of the user; must not be null or empty. 122 | * @param age the age of the user; must not be null and must be greater than or equal to 18. 123 | * @param roles the list of roles assigned to the user; must not be null or empty. 124 | * @throws RuntimeException if any of the validation rules are violated. 125 | */ 126 | public static void processUserWithNestedIfElse(String username, Integer age, List roles) { 127 | 128 | if (username != null) { 129 | if (!username.isEmpty()) { 130 | if (age != null) { 131 | if (age >= 18) { 132 | if (roles != null && !roles.isEmpty()) { 133 | System.out.println("Process user with nested if-else..."); 134 | } else { 135 | throw new RuntimeException("Roles cannot be empty"); 136 | } 137 | } else { 138 | throw new RuntimeException("Age must be greater than 18"); 139 | } 140 | } else { 141 | throw new RuntimeException("Age cannot be null"); 142 | } 143 | } else { 144 | throw new RuntimeException("Username cannot be empty"); 145 | } 146 | } else { 147 | throw new RuntimeException("Username cannot be null"); 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /pattern/factory/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /pattern/factory/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip 20 | -------------------------------------------------------------------------------- /pattern/factory/README.md: -------------------------------------------------------------------------------- 1 | # Java Spring Best Practices: Factory Pattern for Authentications 2 | 3 | This module demonstrates the implementation of the Factory Pattern in a Spring Boot application for handling different 4 | types of authentications. 5 | 6 | 📌 Overview 7 | 8 | This module is designed to demonstrate best practices for implementing the Factory Pattern in a Java Spring application. 9 | The Factory Pattern is used to create objects without specifying the exact class of object that will be created, 10 | promoting loose coupling and extensibility. 11 | 12 | Key Features: 13 | 14 | - **Encapsulation:** Hide the creation logic and create objects through a common interface. 15 | - **Separation of Concerns:** Clean and maintainable code. 16 | - **Dependency Injection:** Uses Spring’s DI to inject dependencies and promote immutability. 17 | 18 | --- 19 | 20 | ## 🏛️ Module Structure 21 | 22 | ```plaintext 23 | src/ 24 | └── main/ 25 | └── java/ 26 | └── dev/ 27 | └── agitrubard/ 28 | └── factory/ 29 | ├── controller/ 30 | │ └── AuthController.java 31 | ├── model/ 32 | │ ├── entity/ 33 | │ │ └── UserEntity.java 34 | │ ├── enums/ 35 | │ │ └── TwoFactorAuthenticationType.java 36 | │ └── request/ 37 | │ ├── LoginRequest.java 38 | │ └── RegisterRequest.java 39 | ├── repository/ 40 | │ ├── UserRepository.java 41 | │ └── impl/ 42 | │ └── UserRepositoryImpl.java 43 | └── service/ 44 | ├── LoginService.java 45 | ├── RegisterService.java 46 | ├── TwoFactorAuthenticationService.java 47 | ├── TwoFactorAuthenticationServiceFactory.java 48 | └── impl/ 49 | ├── EmailAuthenticationServiceImpl.java 50 | ├── LoginServiceImpl.java 51 | ├── PassKeyAuthenticationServiceImpl.java 52 | ├── RegisterServiceImpl.java 53 | ├── SmsAuthenticationServiceImpl.java 54 | └── TwoFactorAuthenticationServiceFactoryImpl.java 55 | ``` 56 | 57 | --- 58 | 59 | ## 🚀 Getting Started 60 | 61 | ### Prerequisites 62 | 63 | - **Java 21** 64 | - **Maven 3.x.x** 65 | - **Spring Boot 3.x.x** 66 | 67 | ### Installation 68 | 69 | **1. Clone the repository:** 70 | 71 | ```bash 72 | git clone https://github.com/agitrubard/java-spring-best-practices.git 73 | cd java-spring-best-practices 74 | ``` 75 | 76 | **2. Build the project:** 77 | 78 | ```bash 79 | mvn clean install 80 | ``` 81 | 82 | **3. Run the application:** 83 | 84 | ```bash 85 | mvn spring-boot:run 86 | ``` 87 | 88 | --- 89 | 90 | ## 📄 Usage 91 | 92 | ### Supported Authentication Types 93 | 94 | - `PASSKEY` 95 | - `EMAIL` 96 | - `SMS` 97 | 98 | ### 1. Registration Flow 99 | 100 | To register a user, make a `POST` request to `/api/v1/auth/register` with the following JSON payload: 101 | 102 | ```json 103 | { 104 | "username": "agitrubard", 105 | "password": "1234", 106 | "emailAddress": "agitrubard@software.eng", 107 | "phoneNumber": "1234567890", 108 | "twoFactorAuthenticationType": "PASSKEY" 109 | } 110 | ``` 111 | 112 | #### Example Response 113 | 114 | ```json 115 | "User authenticating via Passkey..." 116 | ``` 117 | 118 | ### 2. Login Flow 119 | 120 | To login a user, make a `POST` request to `/api/v1/auth/login` with the following JSON payload: 121 | 122 | ```json 123 | { 124 | "username": "agitrubard", 125 | "password": "1234" 126 | } 127 | ``` 128 | 129 | #### Example Response 130 | 131 | ```json 132 | "User authenticating via Passkey..." 133 | ``` 134 | 135 | --- 136 | 137 | ## ⚙️ Factory Pattern Explained 138 | 139 | The **Factory Pattern** is a creational design pattern that provides an interface for creating objects in a superclass, 140 | but allows subclasses to alter the type of objects that will be created. This pattern promotes flexibility and decouples 141 | the client code from the specific classes that implement the creation process. 142 | 143 | ### How It's Applied 144 | 145 | In this project, the `TwoFactorAuthenticationServiceFactory` is responsible for creating the 146 | appropriate `TwoFactorAuthenticationService` 147 | implementation based on the `TwoFactorAuthenticationType`. Each authentication type (Passkey, Email, SMS) has its own 148 | implementation of 149 | the `TwoFactorAuthenticationService` interface: 150 | 151 | - **PassKeyAuthenticationServiceImpl:** Handles Passkey authentications. 152 | - **EmailAuthenticationServiceImpl:** Handles Email authentications. 153 | - **SmsAuthenticationServiceImpl:** Handles SMS authentications. 154 | 155 | The `LoginService` and The `RegisterService` uses the `TwoFactorAuthenticationService` implementations based on the 156 | authentication type specified in 157 | the request. 158 | 159 | ### Benefits 160 | 161 | - **Decoupling:** The client code is not tightly coupled to the concrete classes of the services. 162 | - **Maintainability:** New authentication types can be added without changing existing client code. 163 | - **Flexibility:** Allows runtime decision-making for the creation of objects. 164 | 165 | ### Class Diagram 166 | 167 | ```plaintext 168 | ┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐ 169 | │ RegisterService │ │ LoginService │ 170 | ├─────────────────────────────────────────┤ ├─────────────────────────────────────────┤ 171 | │ + register(RegisterRequest): String │ │ + login(LoginRequest): String │ 172 | └───────────────────┬─────────────────────┘ └────────────────────┬────────────────────┘ 173 | ┌───────────────────┴─────────────────────┐ ┌────────────────────┴────────────────────┐ 174 | │ RegisterServiceImpl │ │ LoginServiceImpl │ 175 | ├─────────────────────────────────────────┤ ├─────────────────────────────────────────┤ 176 | │ - twoFactorAuthenticationServiceFactory │ │ - twoFactorAuthenticationServiceFactory │ 177 | ├─────────────────────────────────────────┤ ├─────────────────────────────────────────┤ 178 | │ + register(RegisterRequest): String │ │ + login(LoginRequest): String │ 179 | └──────────────────┬──────────────────────┘ └───────────────┬─────────────────────────┘ 180 | │ │ 181 | └─────────────┐ ┌───────────────┘ 182 | │ │ 183 | ┌────────────────────────┴─────────────┴────────────────────────────────┐ 184 | │ TwoFactorAuthenticationServiceFactory │ 185 | ├───────────────────────────────────────────────────────────────────────┤ 186 | │ + create(TwoFactorAuthenticationType): TwoFactorAuthenticationService │ 187 | └───────────────────────────────┬───────────────────────────────────────┘ 188 | ┌───────────────────────────────┴───────────────────────────────────────┐ 189 | │ TwoFactorAuthenticationServiceFactoryImpl │ 190 | ├───────────────────────────────────────────────────────────────────────┤ 191 | │ + create(TwoFactorAuthenticationType): TwoFactorAuthenticationService │ 192 | └────────────────────────┬────────┬────────┬────────────────────────────┘ 193 | │ │ │ 194 | ┌────────────────┘ │ └────────────────────────────────────────┐ 195 | │ └─────────────┐ │ 196 | │ │ │ 197 | ┌───────────────┴──────────────────┐ ┌────────────────┴───────────────┐ ┌───────────────┴──────────────┐ 198 | │ PassKeyAuthenticationServiceImpl │ │ EmailAuthenticationServiceImpl │ │ SmsAuthenticationServiceImpl │ 199 | ├──────────────────────────────────┤ ├────────────────────────────────┤ ├──────────────────────────────┤ 200 | │ + authenticate(): String │ │ + authenticate(): String │ │ + authenticate(): String │ 201 | └──────────────────────────────────┘ └────────────────────────────────┘ └──────────────────────────────┘ 202 | ``` -------------------------------------------------------------------------------- /pattern/factory/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /pattern/factory/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pattern/factory/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.4.6 11 | 12 | 13 | 14 | dev.agitrubard 15 | factory 16 | 0.0.1-SNAPSHOT 17 | factory 18 | factory 19 | 20 | 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-maven-plugin 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/FactoryApplication.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class FactoryApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(FactoryApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.controller; 2 | 3 | import dev.agitrubard.factory.model.request.LoginRequest; 4 | import dev.agitrubard.factory.model.request.RegisterRequest; 5 | import dev.agitrubard.factory.service.LoginService; 6 | import dev.agitrubard.factory.service.RegisterService; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/api/v1/auth") 14 | class AuthController { 15 | 16 | private final RegisterService registerService; 17 | private final LoginService loginService; 18 | 19 | public AuthController(RegisterService registerService, LoginService loginService) { 20 | this.registerService = registerService; 21 | this.loginService = loginService; 22 | } 23 | 24 | @PostMapping("/register") 25 | public String register(@RequestBody RegisterRequest registerRequest) { 26 | return registerService.register(registerRequest); 27 | } 28 | 29 | @PostMapping("/login") 30 | public String login(@RequestBody LoginRequest loginRequest) { 31 | return loginService.login(loginRequest); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/model/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.model.entity; 2 | 3 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 4 | 5 | public class UserEntity { 6 | 7 | private String id; 8 | private String username; 9 | private String password; 10 | private String emailAddress; 11 | private String phoneNumber; 12 | private TwoFactorAuthenticationType twoFactorAuthenticationType; 13 | 14 | public UserEntity(String id, 15 | String username, 16 | String password, 17 | String emailAddress, 18 | String phoneNumber, 19 | TwoFactorAuthenticationType twoFactorAuthenticationType) { 20 | 21 | this.id = id; 22 | this.username = username; 23 | this.password = password; 24 | this.emailAddress = emailAddress; 25 | this.phoneNumber = phoneNumber; 26 | this.twoFactorAuthenticationType = twoFactorAuthenticationType; 27 | } 28 | 29 | public String getId() { 30 | return id; 31 | } 32 | 33 | public String getUsername() { 34 | return username; 35 | } 36 | 37 | public String getPassword() { 38 | return password; 39 | } 40 | 41 | public String getEmailAddress() { 42 | return emailAddress; 43 | } 44 | 45 | public String getPhoneNumber() { 46 | return phoneNumber; 47 | } 48 | 49 | public TwoFactorAuthenticationType getTwoFactorAuthenticationType() { 50 | return twoFactorAuthenticationType; 51 | } 52 | 53 | public boolean isTwoFactorAuthenticationNotEnabled() { 54 | return this.twoFactorAuthenticationType == null; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/model/enums/TwoFactorAuthenticationType.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.model.enums; 2 | 3 | public enum TwoFactorAuthenticationType { 4 | 5 | PASSKEY, 6 | SMS, 7 | EMAIL 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/model/request/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.model.request; 2 | 3 | public record LoginRequest(String username, String password) { 4 | 5 | @Override 6 | public String toString() { 7 | return "{" + 8 | "\"username\": \"" + username + "\"," + 9 | "\"password\": \"" + password + "\"" + 10 | "}"; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/model/request/RegisterRequest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.model.request; 2 | 3 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 4 | 5 | public record RegisterRequest(String username, 6 | String password, 7 | String emailAddress, 8 | String phoneNumber, 9 | TwoFactorAuthenticationType twoFactorAuthenticationType) { 10 | 11 | @Override 12 | public String toString() { 13 | 14 | if (this.twoFactorAuthenticationType == null) { 15 | return "{" + 16 | "\"username\": \"" + username + "\"," + 17 | "\"password\": \"" + password + "\"," + 18 | "\"emailAddress\": \"" + emailAddress + "\"," + 19 | "\"phoneNumber\": \"" + phoneNumber + "\"" + 20 | '}'; 21 | } 22 | 23 | return "{" + 24 | "\"username\": \"" + username + "\"," + 25 | "\"password\": \"" + password + "\"," + 26 | "\"emailAddress\": \"" + emailAddress + "\"," + 27 | "\"phoneNumber\": \"" + phoneNumber + "\"," + 28 | "\"twoFactorAuthenticationType\": \"" + twoFactorAuthenticationType + "\"" + 29 | '}'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.repository; 2 | 3 | import dev.agitrubard.factory.model.entity.UserEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public interface UserRepository { 8 | 9 | Optional findByUsername(String username); 10 | 11 | void save(UserEntity userEntity); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/repository/impl/UserRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.repository.impl; 2 | 3 | import dev.agitrubard.factory.model.entity.UserEntity; 4 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 5 | import dev.agitrubard.factory.repository.UserRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | @Repository 14 | class UserRepositoryImpl implements UserRepository { 15 | 16 | private final List users = new ArrayList<>(); 17 | 18 | public UserRepositoryImpl() { 19 | users.add( 20 | new UserEntity( 21 | UUID.randomUUID().toString(), 22 | "user1", 23 | "password1", 24 | "user1@agitrubard.dev", 25 | "1234567890", 26 | TwoFactorAuthenticationType.PASSKEY 27 | ) 28 | ); 29 | 30 | users.add( 31 | new UserEntity( 32 | UUID.randomUUID().toString(), 33 | "user2", 34 | "password2", 35 | "user2@agitrubard.dev", 36 | "1234567891", 37 | TwoFactorAuthenticationType.EMAIL 38 | ) 39 | ); 40 | 41 | users.add( 42 | new UserEntity( 43 | UUID.randomUUID().toString(), 44 | "user3", 45 | "password3", 46 | "user3@agitrubard.dev", 47 | "1234567892", 48 | TwoFactorAuthenticationType.SMS 49 | 50 | ) 51 | ); 52 | } 53 | 54 | @Override 55 | public Optional findByUsername(String username) { 56 | return users.stream() 57 | .filter(user -> user.getUsername().equals(username)) 58 | .findFirst(); 59 | } 60 | 61 | @Override 62 | public void save(UserEntity userEntity) { 63 | users.add(userEntity); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/LoginService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service; 2 | 3 | import dev.agitrubard.factory.model.request.LoginRequest; 4 | 5 | public interface LoginService { 6 | 7 | String login(LoginRequest loginRequest); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/RegisterService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service; 2 | 3 | import dev.agitrubard.factory.model.request.RegisterRequest; 4 | 5 | public interface RegisterService { 6 | 7 | String register(RegisterRequest registerRequest); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/TwoFactorAuthenticationService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service; 2 | 3 | public interface TwoFactorAuthenticationService { 4 | 5 | String authenticate(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/TwoFactorAuthenticationServiceFactory.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service; 2 | 3 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 4 | 5 | public interface TwoFactorAuthenticationServiceFactory { 6 | 7 | TwoFactorAuthenticationService create(TwoFactorAuthenticationType authenticationType); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/EmailAuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 4 | 5 | class EmailAuthenticationServiceImpl implements TwoFactorAuthenticationService { 6 | 7 | @Override 8 | public String authenticate() { 9 | return "User authenticating via Email..."; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/LoginServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.model.entity.UserEntity; 4 | import dev.agitrubard.factory.model.request.LoginRequest; 5 | import dev.agitrubard.factory.repository.UserRepository; 6 | import dev.agitrubard.factory.service.LoginService; 7 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 8 | import dev.agitrubard.factory.service.TwoFactorAuthenticationServiceFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Optional; 12 | 13 | @Service 14 | public class LoginServiceImpl implements LoginService { 15 | 16 | private final UserRepository userRepository; 17 | private final TwoFactorAuthenticationServiceFactory twoFactorAuthenticationServiceFactory; 18 | 19 | public LoginServiceImpl(UserRepository userRepository, TwoFactorAuthenticationServiceFactory twoFactorAuthenticationServiceFactory) { 20 | this.userRepository = userRepository; 21 | this.twoFactorAuthenticationServiceFactory = twoFactorAuthenticationServiceFactory; 22 | } 23 | 24 | @Override 25 | public String login(LoginRequest loginRequest) { 26 | 27 | Optional userEntity = userRepository.findByUsername(loginRequest.username()); 28 | 29 | if (userEntity.isEmpty()) { 30 | return "User not found!"; 31 | } 32 | 33 | if (!userEntity.get().getPassword().equals(loginRequest.password())) { 34 | return "Invalid password!"; 35 | } 36 | 37 | if (userEntity.get().isTwoFactorAuthenticationNotEnabled()) { 38 | return "Login success!"; 39 | } 40 | 41 | TwoFactorAuthenticationService twoFactorAuthenticationService = twoFactorAuthenticationServiceFactory 42 | .create(userEntity.get().getTwoFactorAuthenticationType()); 43 | return twoFactorAuthenticationService.authenticate(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/PassKeyAuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 4 | 5 | class PassKeyAuthenticationServiceImpl implements TwoFactorAuthenticationService { 6 | 7 | @Override 8 | public String authenticate() { 9 | return "User authenticating via Passkey..."; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/RegisterServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.model.entity.UserEntity; 4 | import dev.agitrubard.factory.model.request.RegisterRequest; 5 | import dev.agitrubard.factory.repository.UserRepository; 6 | import dev.agitrubard.factory.service.RegisterService; 7 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 8 | import dev.agitrubard.factory.service.TwoFactorAuthenticationServiceFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.UUID; 12 | 13 | @Service 14 | class RegisterServiceImpl implements RegisterService { 15 | 16 | private final UserRepository userRepository; 17 | private final TwoFactorAuthenticationServiceFactory twoFactorAuthenticationServiceFactory; 18 | 19 | public RegisterServiceImpl(UserRepository userRepository, TwoFactorAuthenticationServiceFactory twoFactorAuthenticationServiceFactory) { 20 | this.userRepository = userRepository; 21 | this.twoFactorAuthenticationServiceFactory = twoFactorAuthenticationServiceFactory; 22 | } 23 | 24 | @Override 25 | public String register(RegisterRequest registerRequest) { 26 | 27 | UserEntity userEntity = new UserEntity( 28 | UUID.randomUUID().toString(), 29 | registerRequest.username(), 30 | registerRequest.password(), 31 | registerRequest.emailAddress(), 32 | registerRequest.phoneNumber(), 33 | registerRequest.twoFactorAuthenticationType() 34 | ); 35 | 36 | userRepository.save(userEntity); 37 | 38 | TwoFactorAuthenticationService twoFactorAuthenticationService = twoFactorAuthenticationServiceFactory 39 | .create(registerRequest.twoFactorAuthenticationType()); 40 | return twoFactorAuthenticationService.authenticate(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/SmsAuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 4 | 5 | class SmsAuthenticationServiceImpl implements TwoFactorAuthenticationService { 6 | 7 | @Override 8 | public String authenticate() { 9 | return "User authenticating via SMS..."; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pattern/factory/src/main/java/dev/agitrubard/factory/service/impl/TwoFactorAuthenticationServiceFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.service.impl; 2 | 3 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 4 | import dev.agitrubard.factory.service.TwoFactorAuthenticationService; 5 | import dev.agitrubard.factory.service.TwoFactorAuthenticationServiceFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | @Component 11 | class TwoFactorAuthenticationServiceFactoryImpl implements TwoFactorAuthenticationServiceFactory { 12 | 13 | @Override 14 | public TwoFactorAuthenticationService create(TwoFactorAuthenticationType twoFactorAuthenticationType) { 15 | 16 | TwoFactorAuthenticationType authenticationType = Optional.ofNullable(twoFactorAuthenticationType) 17 | .orElse(TwoFactorAuthenticationType.EMAIL); 18 | 19 | return switch (authenticationType) { 20 | case PASSKEY -> new PassKeyAuthenticationServiceImpl(); 21 | case SMS -> new SmsAuthenticationServiceImpl(); 22 | case EMAIL -> new EmailAuthenticationServiceImpl(); 23 | }; 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /pattern/factory/src/test/java/dev/agitrubard/factory/controller/FactoryPatternEndToEndTest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.factory.controller; 2 | 3 | import dev.agitrubard.factory.model.enums.TwoFactorAuthenticationType; 4 | import dev.agitrubard.factory.model.request.LoginRequest; 5 | import dev.agitrubard.factory.model.request.RegisterRequest; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 15 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 16 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 17 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 18 | 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | @ExtendWith(MockitoExtension.class) 22 | class FactoryPatternEndToEndTest { 23 | 24 | @Autowired 25 | private MockMvc mockMvc; 26 | 27 | 28 | private static final String REGISTER_ENDPOINT = "/api/v1/auth/register"; 29 | private static final String LOGIN_ENDPOINT = "/api/v1/auth/login"; 30 | 31 | 32 | private static final String PASSKEY_AUTHENTICATION_RESPONSE_MESSAGE = "User authenticating via Passkey..."; 33 | private static final String EMAIL_AUTHENTICATION_RESPONSE_MESSAGE = "User authenticating via Email..."; 34 | private static final String SMS_AUTHENTICATION_RESPONSE_MESSAGE = "User authenticating via SMS..."; 35 | 36 | 37 | /** 38 | * End to end tests for the register and login endpoint 39 | */ 40 | @Test 41 | void givenValidRegisterAndLoginRequests_whenUserAuthenticatesWithPasskey_thenReturnsRelatedMessage() throws Exception { 42 | 43 | // Given - Register 44 | RegisterRequest registerRequest = new RegisterRequest( 45 | "agitrubard", 46 | "1234", 47 | "agitrubard@software.eng", 48 | "1234567890", 49 | TwoFactorAuthenticationType.PASSKEY 50 | ); 51 | 52 | // Then - Register 53 | MockHttpServletRequestBuilder mockHttpServletRegisterRequestBuilder = MockMvcRequestBuilders 54 | .post(REGISTER_ENDPOINT) 55 | .content(registerRequest.toString()) 56 | .contentType(MediaType.APPLICATION_JSON); 57 | 58 | mockMvc.perform(mockHttpServletRegisterRequestBuilder) 59 | .andDo(MockMvcResultHandlers.print()) 60 | .andExpect(MockMvcResultMatchers.jsonPath("$") 61 | .value(PASSKEY_AUTHENTICATION_RESPONSE_MESSAGE)); 62 | 63 | 64 | // Given - Login 65 | LoginRequest loginRequest = new LoginRequest( 66 | "agitrubard", 67 | "1234" 68 | ); 69 | 70 | // Then - Login 71 | MockHttpServletRequestBuilder mockHttpServletLoginRequestBuilder = MockMvcRequestBuilders 72 | .post(LOGIN_ENDPOINT) 73 | .content(loginRequest.toString()) 74 | .contentType(MediaType.APPLICATION_JSON); 75 | 76 | mockMvc.perform(mockHttpServletLoginRequestBuilder) 77 | .andDo(MockMvcResultHandlers.print()) 78 | .andExpect(MockMvcResultMatchers.jsonPath("$") 79 | .value(PASSKEY_AUTHENTICATION_RESPONSE_MESSAGE)); 80 | } 81 | 82 | 83 | /** 84 | * End to end tests for the register endpoint 85 | */ 86 | @Test 87 | void givenRegisterRequestWithoutTwoFactorAuthentication_whenUserAuthenticatesViaDefaultAuthenticationType_thenReturnRelatedMessage() throws Exception { 88 | // Given 89 | RegisterRequest registerRequest = new RegisterRequest( 90 | "agitrubard1", 91 | "1234", 92 | "agitrubard1@software.eng", 93 | "1234567891", 94 | null 95 | ); 96 | 97 | // Then 98 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 99 | .post(REGISTER_ENDPOINT) 100 | .content(registerRequest.toString()) 101 | .contentType(MediaType.APPLICATION_JSON); 102 | 103 | mockMvc.perform(mockHttpServletRequestBuilder) 104 | .andDo(MockMvcResultHandlers.print()) 105 | .andExpect(MockMvcResultMatchers.jsonPath("$") 106 | .value(EMAIL_AUTHENTICATION_RESPONSE_MESSAGE)); 107 | } 108 | 109 | @Test 110 | void givenRegisterRequestWithTwoFactorAuthentication_whenUserAuthenticatesViaPasskey_thenReturnRelatedMessage() throws Exception { 111 | // Given 112 | TwoFactorAuthenticationType authenticationType = TwoFactorAuthenticationType.PASSKEY; 113 | RegisterRequest registerRequest = new RegisterRequest( 114 | "agitrubard2", 115 | "1234", 116 | "agitrubard2@software.eng", 117 | "1234567892", 118 | authenticationType 119 | ); 120 | 121 | // Then 122 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 123 | .post(REGISTER_ENDPOINT) 124 | .content(registerRequest.toString()) 125 | .contentType(MediaType.APPLICATION_JSON); 126 | 127 | mockMvc.perform(mockHttpServletRequestBuilder) 128 | .andDo(MockMvcResultHandlers.print()) 129 | .andExpect(MockMvcResultMatchers.jsonPath("$") 130 | .value(PASSKEY_AUTHENTICATION_RESPONSE_MESSAGE)); 131 | } 132 | 133 | @Test 134 | void givenRegisterRequestWithTwoFactorAuthentication_whenUserAuthenticatesViaEmail_thenReturnRelatedMessage() throws Exception { 135 | // Given 136 | TwoFactorAuthenticationType authenticationType = TwoFactorAuthenticationType.EMAIL; 137 | RegisterRequest registerRequest = new RegisterRequest( 138 | "agitrubard3", 139 | "1234", 140 | "agitrubard3@software.eng", 141 | "1234567893", 142 | authenticationType 143 | ); 144 | 145 | // Then 146 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 147 | .post(REGISTER_ENDPOINT) 148 | .content(registerRequest.toString()) 149 | .contentType(MediaType.APPLICATION_JSON); 150 | 151 | mockMvc.perform(mockHttpServletRequestBuilder) 152 | .andDo(MockMvcResultHandlers.print()) 153 | .andExpect(MockMvcResultMatchers.jsonPath("$") 154 | .value(EMAIL_AUTHENTICATION_RESPONSE_MESSAGE)); 155 | } 156 | 157 | @Test 158 | void givenRegisterRequestWithTwoFactorAuthentication_whenUserAuthenticatesViaSMS_thenReturnRelatedMessage() throws Exception { 159 | // Given 160 | TwoFactorAuthenticationType authenticationType = TwoFactorAuthenticationType.SMS; 161 | RegisterRequest registerRequest = new RegisterRequest( 162 | "agitrubard4", 163 | "1234", 164 | "agitrubard4@software.eng", 165 | "1234567894", 166 | authenticationType 167 | ); 168 | 169 | // Then 170 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 171 | .post(REGISTER_ENDPOINT) 172 | .content(registerRequest.toString()) 173 | .contentType(MediaType.APPLICATION_JSON); 174 | 175 | mockMvc.perform(mockHttpServletRequestBuilder) 176 | .andDo(MockMvcResultHandlers.print()) 177 | .andExpect(MockMvcResultMatchers.jsonPath("$") 178 | .value(SMS_AUTHENTICATION_RESPONSE_MESSAGE)); 179 | } 180 | 181 | 182 | /** 183 | * End to end tests for the login endpoint 184 | */ 185 | @Test 186 | void givenLoginRequest_whenUserAuthenticatesViaPasskey_thenReturnRelatedMessage() throws Exception { 187 | 188 | // Given 189 | LoginRequest loginRequest = new LoginRequest( 190 | "user1", 191 | "password1" 192 | ); 193 | 194 | // Then 195 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 196 | .post(LOGIN_ENDPOINT) 197 | .content(loginRequest.toString()) 198 | .contentType(MediaType.APPLICATION_JSON); 199 | 200 | mockMvc.perform(mockHttpServletRequestBuilder) 201 | .andDo(MockMvcResultHandlers.print()) 202 | .andExpect(MockMvcResultMatchers.jsonPath("$") 203 | .value(PASSKEY_AUTHENTICATION_RESPONSE_MESSAGE)); 204 | } 205 | 206 | @Test 207 | void givenLoginRequest_whenUserAuthenticatesViaEmail_thenReturnRelatedMessage() throws Exception { 208 | 209 | // Given 210 | LoginRequest loginRequest = new LoginRequest( 211 | "user2", 212 | "password2" 213 | ); 214 | 215 | // Then 216 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 217 | .post(LOGIN_ENDPOINT) 218 | .content(loginRequest.toString()) 219 | .contentType(MediaType.APPLICATION_JSON); 220 | 221 | mockMvc.perform(mockHttpServletRequestBuilder) 222 | .andDo(MockMvcResultHandlers.print()) 223 | .andExpect(MockMvcResultMatchers.jsonPath("$") 224 | .value(EMAIL_AUTHENTICATION_RESPONSE_MESSAGE)); 225 | } 226 | 227 | @Test 228 | void givenLoginRequest_whenUserAuthenticatesViaSMS_thenReturnRelatedMessage() throws Exception { 229 | 230 | // Given 231 | LoginRequest loginRequest = new LoginRequest( 232 | "user3", 233 | "password3" 234 | ); 235 | 236 | // Then 237 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 238 | .post(LOGIN_ENDPOINT) 239 | .content(loginRequest.toString()) 240 | .contentType(MediaType.APPLICATION_JSON); 241 | 242 | mockMvc.perform(mockHttpServletRequestBuilder) 243 | .andDo(MockMvcResultHandlers.print()) 244 | .andExpect(MockMvcResultMatchers.jsonPath("$") 245 | .value(SMS_AUTHENTICATION_RESPONSE_MESSAGE)); 246 | } 247 | 248 | } -------------------------------------------------------------------------------- /pattern/strategy/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /pattern/strategy/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.1 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 19 | -------------------------------------------------------------------------------- /pattern/strategy/README.md: -------------------------------------------------------------------------------- 1 | # Java Spring Best Practices: Strategy Pattern for Notifications 2 | 3 | This module demonstrates the implementation of the Strategy Pattern in a Spring Boot application for handling different 4 | types of notifications. 5 | 6 | --- 7 | 8 | ## 📌 Overview 9 | 10 | This module is designed to demonstrate best practices for implementing the **Strategy Pattern** in a Java Spring 11 | application. The Strategy Pattern is used to encapsulate a family of algorithms and allows the algorithm to vary 12 | independently from the clients that use it. 13 | 14 | ### Key Features: 15 | 16 | - **Modular Design:** Easily extend or modify notification types. 17 | - **Separation of Concerns:** Clean and maintainable code. 18 | - **Dependency Injection:** Uses Spring's DI to inject dependencies and promote immutability. 19 | 20 | --- 21 | 22 | ## 🏛️ Module Structure 23 | 24 | ```plaintext 25 | src/ 26 | ├── main/ 27 | │ └── java/ 28 | │ └── dev/ 29 | │ └── agitrubard/ 30 | │ └── strategy/ 31 | │ ├── controller/ 32 | │ │ └── NotificationController.java 33 | │ ├── model/ 34 | │ │ ├── enums/ 35 | │ │ │ └── NotificationType.java 36 | │ │ └── request/ 37 | │ │ └── NotificationRequest.java 38 | │ └── service/ 39 | │ ├── NotificationService.java 40 | │ └── impl/ 41 | │ ├── EmailNotificationServiceImpl.java 42 | │ ├── PushNotificationServiceImpl.java 43 | │ └── SmsNotificationServiceImpl.java 44 | └── test/ 45 | └── java/ 46 | └── dev/ 47 | └── agitrubard/ 48 | └── strategy/ 49 | └── controller/ 50 | └── StrategyPatternEndToEndTest.java 51 | ``` 52 | 53 | --- 54 | 55 | ## 🚀 Getting Started 56 | 57 | ### Prerequisites 58 | 59 | - **Java 21** 60 | - **Maven 3.x.x** 61 | - **Spring Boot 3.x.x** 62 | 63 | ### Installation 64 | 65 | **1. Clone the repository:** 66 | 67 | ```bash 68 | git clone https://github.com/agitrubard/java-spring-best-practices.git 69 | cd java-spring-best-practices 70 | ``` 71 | 72 | **2. Build the project:** 73 | 74 | ```bash 75 | mvn clean install 76 | ``` 77 | 78 | **3. Run the application:** 79 | 80 | ```bash 81 | mvn spring-boot:run 82 | ``` 83 | 84 | --- 85 | 86 | ## 📄 Usage 87 | 88 | ### Supported Notification Types 89 | 90 | - **EMAIL** 91 | - **SMS** 92 | - **PUSH** 93 | 94 | ### Sending Notifications 95 | 96 | To send a notification, make a `POST` request to `/api/v1/notifications/send` with the following JSON payload: 97 | 98 | ```json 99 | { 100 | "to": "user@example.com", 101 | "type": "EMAIL" 102 | } 103 | ``` 104 | 105 | #### Example Response 106 | 107 | ```json 108 | "Email notification sent to user@example.com" 109 | ``` 110 | 111 | --- 112 | 113 | ## ⚙️ Strategy Pattern Explained 114 | 115 | The **Strategy Pattern** is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime. It 116 | defines a family of algorithms, encapsulates each one, and makes them interchangeable. The strategy pattern lets the 117 | algorithm vary independently from clients that use it. 118 | 119 | ### How It's Applied 120 | 121 | In this project, the `NotificationService` interface represents the strategy interface. Each notification type (Email, 122 | Push, SMS) has its own implementation of this interface: 123 | 124 | - **EmailNotificationServiceImpl:** Handles email notifications. 125 | - **PushNotificationServiceImpl:** Handles push notifications. 126 | - **SmsNotificationServiceImpl:** Handles SMS notifications. 127 | 128 | The `NotificationController` uses the `NotificationService` implementations based on the notification type specified in 129 | the request. 130 | 131 | ### Benefits 132 | 133 | - **Flexibility:** Easily add new notification types without modifying existing code. 134 | - **Maintainability:** Each notification type is encapsulated in its own class, adhering to the Single Responsibility 135 | Principle. 136 | 137 | ### Class Diagram 138 | 139 | ```plaintext 140 | ┌────────────────────────┐ 141 | │ NotificationController │ 142 | ├────────────────────────┤ 143 | │ - notificationServices │ 144 | └───────────┬────────────┘ 145 | │ 146 | │ 147 | ┌───────────┴─────────────────────────┐ 148 | │ NotificationService │ 149 | ├─────────────────────────────────────┤ 150 | │ + getType(): NotificationType │ 151 | │ + send(NotificationRequest): String │ 152 | └──────────┬────────┬────────┬────────┘ 153 | │ │ │ 154 | │ │ │ 155 | │ │ │ 156 | │ │ └─────────────────────────────────────────────────────┐ 157 | │ │ │ 158 | │ └─────────────────────────────┐ │ 159 | │ │ │ 160 | │ │ │ 161 | ┌──────────┴───────────────────┐ ┌──────────────┴──────────────┐ ┌─────────────┴───────────────┐ 162 | │ EmailNotificationServiceImpl │ │ PushNotificationServiceImpl │ │ SmsNotificationServiceImpl │ 163 | ├──────────────────────────────┤ ├─────────────────────────────┤ ├─────────────────────────────┤ 164 | │ + getType() │ │ + getType() │ │ + getType() │ 165 | │ + send(NotificationRequest) │ │ + send(NotificationRequest) │ │ + send(NotificationRequest) │ 166 | └──────────────────────────────┘ └─────────────────────────────┘ └─────────────────────────────┘ 167 | ``` -------------------------------------------------------------------------------- /pattern/strategy/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # apply MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /pattern/strategy/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - userEntity and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /pattern/strategy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.4.6 11 | 12 | 13 | 14 | dev.agitrubard 15 | strategy 16 | 0.0.1-SNAPSHOT 17 | strategy 18 | strategy 19 | 20 | 21 | 21 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-maven-plugin 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/StrategyApplication.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StrategyApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StrategyApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/controller/NotificationController.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.controller; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | import dev.agitrubard.strategy.service.NotificationService; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/api/v1/notifications") 15 | class NotificationController { 16 | 17 | private final List notificationServices; 18 | 19 | public NotificationController(List notificationServices) { 20 | this.notificationServices = notificationServices; 21 | } 22 | 23 | @PostMapping("/send") 24 | public String sendNotification(@RequestBody NotificationRequest notificationRequest) { 25 | NotificationService notificationService = this.findNotificationService(notificationRequest.type()); 26 | return notificationService.send(notificationRequest); 27 | } 28 | 29 | private NotificationService findNotificationService(NotificationType type) { 30 | return notificationServices.stream() 31 | .filter(notificationService -> notificationService.getType().equals(type)) 32 | .findFirst() 33 | .orElseThrow(() -> new IllegalArgumentException("Notification type not implemented")); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/model/enums/NotificationType.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.model.enums; 2 | 3 | public enum NotificationType { 4 | 5 | EMAIL, 6 | PUSH, 7 | SMS 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/model/request/NotificationRequest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.model.request; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | 5 | public record NotificationRequest(String to, NotificationType type) { 6 | 7 | @Override 8 | public String toString() { 9 | return "{" + 10 | "\"to\": \"" + to + "\"," + 11 | "\"type\": \"" + type + "\"" + 12 | '}'; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/service/NotificationService.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.service; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | 6 | public interface NotificationService { 7 | 8 | NotificationType getType(); 9 | 10 | String send(NotificationRequest notificationRequest); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/service/impl/EmailNotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.service.impl; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | import dev.agitrubard.strategy.service.NotificationService; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | class EmailNotificationServiceImpl implements NotificationService { 10 | 11 | @Override 12 | public NotificationType getType() { 13 | return NotificationType.EMAIL; 14 | } 15 | 16 | @Override 17 | public String send(NotificationRequest notificationRequest) { 18 | return "Email notification sent to " + notificationRequest.to(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/service/impl/PushNotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.service.impl; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | import dev.agitrubard.strategy.service.NotificationService; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | class PushNotificationServiceImpl implements NotificationService { 10 | 11 | @Override 12 | public NotificationType getType() { 13 | return NotificationType.PUSH; 14 | } 15 | 16 | @Override 17 | public String send(NotificationRequest notificationRequest) { 18 | return "Push notification sent to " + notificationRequest.to(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /pattern/strategy/src/main/java/dev/agitrubard/strategy/service/impl/SmsNotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.service.impl; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | import dev.agitrubard.strategy.service.NotificationService; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | class SmsNotificationServiceImpl implements NotificationService { 10 | 11 | @Override 12 | public NotificationType getType() { 13 | return NotificationType.SMS; 14 | } 15 | 16 | @Override 17 | public String send(NotificationRequest notificationRequest) { 18 | return "Sms notification sent to " + notificationRequest.to(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /pattern/strategy/src/test/java/dev/agitrubard/strategy/controller/StrategyPatternEndToEndTest.java: -------------------------------------------------------------------------------- 1 | package dev.agitrubard.strategy.controller; 2 | 3 | import dev.agitrubard.strategy.model.enums.NotificationType; 4 | import dev.agitrubard.strategy.model.request.NotificationRequest; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 14 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 15 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 16 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 17 | 18 | @SpringBootTest 19 | @AutoConfigureMockMvc 20 | @ExtendWith(MockitoExtension.class) 21 | class StrategyPatternEndToEndTest { 22 | 23 | @Autowired 24 | private MockMvc mockMvc; 25 | 26 | @Test 27 | void givenValidNotificationRequest_whenEmailNotificationSent_thenReturnMessage() throws Exception { 28 | // Given 29 | NotificationType notificationType = NotificationType.EMAIL; 30 | NotificationRequest notificationRequest = new NotificationRequest( 31 | "agitrubard@software.eng", 32 | notificationType 33 | ); 34 | 35 | // Then 36 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 37 | .post("/api/v1/notifications/send") 38 | .content(notificationRequest.toString()) 39 | .contentType(MediaType.APPLICATION_JSON); 40 | 41 | mockMvc.perform(mockHttpServletRequestBuilder) 42 | .andDo(MockMvcResultHandlers.print()) 43 | .andExpect(MockMvcResultMatchers.jsonPath("$") 44 | .value("Email notification sent to agitrubard@software.eng")); 45 | } 46 | 47 | @Test 48 | void givenValidNotificationRequest_whenPushNotificationSent_thenReturnMessage() throws Exception { 49 | // Given 50 | NotificationType notificationType = NotificationType.PUSH; 51 | NotificationRequest notificationRequest = new NotificationRequest( 52 | "agitrubard@software.eng", 53 | notificationType 54 | ); 55 | 56 | // Then 57 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 58 | .post("/api/v1/notifications/send") 59 | .content(notificationRequest.toString()) 60 | .contentType(MediaType.APPLICATION_JSON); 61 | 62 | mockMvc.perform(mockHttpServletRequestBuilder) 63 | .andDo(MockMvcResultHandlers.print()) 64 | .andExpect(MockMvcResultMatchers.jsonPath("$") 65 | .value("Push notification sent to agitrubard@software.eng")); 66 | } 67 | 68 | @Test 69 | void givenValidNotificationRequest_whenSMSNotificationSent_thenReturnMessage() throws Exception { 70 | // Given 71 | NotificationType notificationType = NotificationType.SMS; 72 | NotificationRequest notificationRequest = new NotificationRequest( 73 | "agitrubard@software.eng", 74 | notificationType 75 | ); 76 | 77 | // Then 78 | MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders 79 | .post("/api/v1/notifications/send") 80 | .content(notificationRequest.toString()) 81 | .contentType(MediaType.APPLICATION_JSON); 82 | 83 | mockMvc.perform(mockHttpServletRequestBuilder) 84 | .andDo(MockMvcResultHandlers.print()) 85 | .andExpect(MockMvcResultMatchers.jsonPath("$") 86 | .value("Sms notification sent to agitrubard@software.eng")); 87 | } 88 | 89 | } --------------------------------------------------------------------------------