├── .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 | *
12 | *
{@link #processUserWithGuardClause(String, Integer, List)}: Uses guard clauses to validate user input.
13 | *
{@link #processUserWithNestedIfElse(String, Integer, List)}: Uses nested if-else statements for validation.
14 | *
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 | *
21 | *
Readability: Guard clauses make the code easier to read by separating validation checks
22 | * into clear, independent blocks. Each check is concise and exits the method early if a condition is violated.
23 | *
Maintainability: Adding or modifying validation rules is straightforward since each rule is isolated.
24 | * You can easily insert, remove, or change conditions without affecting unrelated logic.
25 | *
Reduced Complexity: Guard clauses prevent deeply nested structures, making it easier to understand
26 | * the overall flow of the method.
27 | *
Failure Early: Each invalid condition is detected and handled as soon as it is encountered, improving
28 | * the clarity of failure points.
29 | *
30 | *
31 | * Nested If-Else Statements:
32 | *
33 | *
Poor Readability: Deeply nested structures make the code harder to follow. Readers must keep track
34 | * of multiple conditions simultaneously.
35 | *
Maintainability Challenges: Adding or changing validation rules requires careful restructuring of
36 | * nested conditions, increasing the risk of introducing bugs.
37 | *
High Cognitive Load: Developers need to mentally track multiple layers of conditions,
38 | * which increases the complexity of understanding and debugging the code.
39 | *
Unnecessary Indentation: Multiple levels of indentation make the code harder to scan and less aesthetically pleasing.
40 | *
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 | *
71 | *
Username must not be null or empty.
72 | *
Age must not be null and must be greater than or equal to 18.
73 | *
Roles list must not be null or empty.
74 | *
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 | *
116 | *
Username must not be null or empty.
117 | *
Age must not be null and must be greater than or equal to 18.
118 | *
Roles list must not be null or empty.
119 | *
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 | }
--------------------------------------------------------------------------------