├── images ├── mailhog-ui.png ├── mailtrap-ui.png ├── pdf-example.png ├── 1-swagger-ui.png ├── 2-swagger-ui.png ├── mailtrap-config.png ├── server-details-log.png ├── 1-sonarqube-login-ui.png ├── 2-sonarqube-homepage.png ├── mailhog-email-message.png ├── mailhog-example-email.png ├── 3-sonarqube-my-account.png ├── 4-sonarqube-generate-token.png ├── 5-sonarqube-added-project.png └── mailhog-email-attachments.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── reportLogo │ │ │ ├── logo.png │ │ │ ├── luffy.png │ │ │ └── watermark.png │ │ ├── messages_es.properties │ │ ├── db │ │ │ └── migration │ │ │ │ ├── V2__insert_department_records.sql │ │ │ │ ├── V1__init_schemas.sql │ │ │ │ └── V3__insert_employee_records.sql │ │ ├── messages.properties │ │ ├── messages_el.properties │ │ ├── messages_de.properties │ │ ├── application.yaml │ │ ├── templates │ │ │ └── email-template.html │ │ └── jrxml │ │ │ └── excel │ │ │ └── multiSheetExcelReport.jrxml │ └── java │ │ └── com │ │ └── ainigma100 │ │ └── departmentapi │ │ ├── service │ │ ├── EmailService.java │ │ ├── DepartmentService.java │ │ ├── ReportService.java │ │ ├── EmployeeService.java │ │ └── impl │ │ │ ├── EmailServiceImpl.java │ │ │ ├── DepartmentServiceImpl.java │ │ │ └── EmployeeServiceImpl.java │ │ ├── enums │ │ ├── Status.java │ │ └── ReportLanguage.java │ │ ├── dto │ │ ├── ErrorDTO.java │ │ ├── EmployeeDTO.java │ │ ├── FileDTO.java │ │ ├── DepartmentDTO.java │ │ ├── APIResponse.java │ │ ├── DepartmentReportDTO.java │ │ ├── EmployeeReportDTO.java │ │ ├── EmployeeAndDepartmentDTO.java │ │ ├── DepartmentRequestDTO.java │ │ ├── EmployeeSearchCriteriaDTO.java │ │ ├── EmployeeRequestDTO.java │ │ └── DepartmentSearchCriteriaDTO.java │ │ ├── exception │ │ ├── ReportGenerationException.java │ │ ├── BusinessLogicException.java │ │ └── GlobalExceptionHandler.java │ │ ├── utils │ │ ├── annotation │ │ │ ├── ExecutionTime.java │ │ │ └── ExecutionTimeCalculator.java │ │ ├── SortItem.java │ │ ├── email │ │ │ ├── EmailRequest.java │ │ │ ├── MailHealthIndicator.java │ │ │ └── EmailSender.java │ │ ├── jasperreport │ │ │ ├── SimpleReportFiller.java │ │ │ └── SimpleReportExporter.java │ │ └── Utils.java │ │ ├── config │ │ ├── JacksonConfig.java │ │ ├── InternationalizationConfig.java │ │ └── OpenApiConfig.java │ │ ├── DepartmentApiApplication.java │ │ ├── entity │ │ ├── Employee.java │ │ └── Department.java │ │ ├── repository │ │ ├── DepartmentRepository.java │ │ └── EmployeeRepository.java │ │ ├── filter │ │ ├── FiltersConfig.java │ │ ├── LoggingFilter.java │ │ ├── RateLimitingFilter.java │ │ └── ServerDetails.java │ │ ├── mapper │ │ ├── DepartmentMapper.java │ │ └── EmployeeMapper.java │ │ └── controller │ │ ├── EmailController.java │ │ ├── DepartmentController.java │ │ ├── EmployeeController.java │ │ └── ReportController.java └── test │ ├── resources │ ├── application-test.yaml │ └── application-testcontainers.yaml │ └── java │ └── com │ └── ainigma100 │ └── departmentapi │ ├── utils │ └── TestHelper.java │ ├── integration │ ├── AbstractContainerBaseTest.java │ ├── EmailControllerIntegrationTest.java │ └── ReportControllerIntegrationTest.java │ ├── controller │ ├── EmailControllerTest.java │ └── ReportControllerTest.java │ ├── repository │ ├── DepartmentRepositoryTest.java │ └── EmployeeRepositoryTest.java │ └── service │ └── impl │ └── EmailServiceImplTest.java ├── Dockerfile ├── .gitignore ├── docker-compose.yaml ├── mvnw.cmd ├── mvnw └── pom.xml /images/mailhog-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailhog-ui.png -------------------------------------------------------------------------------- /images/mailtrap-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailtrap-ui.png -------------------------------------------------------------------------------- /images/pdf-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/pdf-example.png -------------------------------------------------------------------------------- /images/1-swagger-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/1-swagger-ui.png -------------------------------------------------------------------------------- /images/2-swagger-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/2-swagger-ui.png -------------------------------------------------------------------------------- /images/mailtrap-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailtrap-config.png -------------------------------------------------------------------------------- /images/server-details-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/server-details-log.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /images/1-sonarqube-login-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/1-sonarqube-login-ui.png -------------------------------------------------------------------------------- /images/2-sonarqube-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/2-sonarqube-homepage.png -------------------------------------------------------------------------------- /images/mailhog-email-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailhog-email-message.png -------------------------------------------------------------------------------- /images/mailhog-example-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailhog-example-email.png -------------------------------------------------------------------------------- /images/3-sonarqube-my-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/3-sonarqube-my-account.png -------------------------------------------------------------------------------- /images/4-sonarqube-generate-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/4-sonarqube-generate-token.png -------------------------------------------------------------------------------- /images/5-sonarqube-added-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/5-sonarqube-added-project.png -------------------------------------------------------------------------------- /images/mailhog-email-attachments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/images/mailhog-email-attachments.png -------------------------------------------------------------------------------- /src/main/resources/reportLogo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/src/main/resources/reportLogo/logo.png -------------------------------------------------------------------------------- /src/main/resources/reportLogo/luffy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/src/main/resources/reportLogo/luffy.png -------------------------------------------------------------------------------- /src/main/resources/messages_es.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/src/main/resources/messages_es.properties -------------------------------------------------------------------------------- /src/main/resources/reportLogo/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmoustopoulos/department-api/HEAD/src/main/resources/reportLogo/watermark.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-oracle 2 | 3 | WORKDIR /opt 4 | 5 | COPY target/department-api-0.0.1-SNAPSHOT.jar /opt/department-api.jar 6 | 7 | ENTRYPOINT ["java", "-jar", "department-api.jar" ] -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service; 2 | 3 | 4 | import net.sf.jasperreports.engine.JRException; 5 | 6 | public interface EmailService { 7 | 8 | Boolean sendEmailWithoutAttachment(); 9 | 10 | Boolean sendEmailWithAttachment() throws JRException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/enums/Status.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum Status { 9 | SUCCESS("Success"), 10 | FAILED("Failed"); 11 | 12 | private final String value; 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/ErrorDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class ErrorDTO { 11 | 12 | private String field; 13 | private String errorMessage; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/exception/ReportGenerationException.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.exception; 2 | 3 | public class ReportGenerationException extends RuntimeException { 4 | 5 | 6 | public ReportGenerationException(String message) { 7 | super(message); 8 | } 9 | 10 | public ReportGenerationException(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/annotation/ExecutionTime.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.annotation; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ METHOD }) 10 | @Retention(RUNTIME) 11 | public @interface ExecutionTime { 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/config/JacksonConfig.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 5 | 6 | @Configuration 7 | @EnableSpringDataWebSupport( 8 | pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO 9 | ) 10 | public class JacksonConfig { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/EmployeeDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.math.BigDecimal; 6 | 7 | @Getter 8 | @Setter 9 | @ToString 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class EmployeeDTO { 13 | 14 | private String id; 15 | private String firstName; 16 | private String lastName; 17 | private String email; 18 | private BigDecimal salary; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/FileDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class FileDTO { 13 | 14 | private String fileName; 15 | private byte[] fileContent; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/DepartmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.util.Set; 6 | 7 | @Getter 8 | @Setter 9 | @ToString 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DepartmentDTO { 13 | 14 | private Long id; 15 | private String departmentCode; 16 | private String departmentName; 17 | private String departmentDescription; 18 | private Set employees; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/SortItem.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import org.springframework.data.domain.Sort; 6 | 7 | import java.io.Serializable; 8 | 9 | @Getter 10 | public class SortItem implements Serializable { 11 | 12 | 13 | @Schema(example = "id") // set a default sorting property for swagger 14 | private String field; 15 | private Sort.Direction direction; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/DepartmentApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | 7 | @EnableAsync 8 | @SpringBootApplication 9 | public class DepartmentApiApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(DepartmentApiApplication.class, args); 13 | } 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | flyway: 3 | enabled: true 4 | validate-on-migrate: true 5 | locations: classpath:db/migration 6 | datasource: 7 | url: jdbc:h2:mem:testdb 8 | driverClassName: org.h2.Driver 9 | username: sa 10 | password: 11 | jpa: 12 | hibernate: 13 | ddl-auto: validate 14 | properties: 15 | hibernate: 16 | dialect: org.hibernate.dialect.H2Dialect 17 | 18 | # Bucket4j to handle rate limit 19 | rate-limiting: 20 | max-requests: 10 21 | refill-duration: 1 22 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/exception/BusinessLogicException.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.exception; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @Getter 8 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 9 | public class BusinessLogicException extends RuntimeException { 10 | 11 | 12 | private String message; 13 | 14 | public BusinessLogicException(String message) { 15 | this.message = message; 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.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 | 35 | data/ 36 | 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/APIResponse.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.List; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | @Builder 16 | public class APIResponse { 17 | 18 | private String status; 19 | private List errors; 20 | private T results; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/DepartmentReportDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Getter 8 | @Setter 9 | @ToString 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DepartmentReportDTO { 13 | 14 | private Long id; 15 | private String departmentCode; 16 | private String departmentName; 17 | private String departmentDescription; 18 | private Integer totalEmployees; 19 | private LocalDateTime createdAt; 20 | private LocalDateTime updatedAt; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/EmployeeReportDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.LocalDateTime; 7 | 8 | @Getter 9 | @Setter 10 | @ToString 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class EmployeeReportDTO { 14 | 15 | private String firstName; 16 | private String lastName; 17 | private String email; 18 | private BigDecimal salary; 19 | private String departmentName; 20 | private LocalDateTime createdAt; 21 | private LocalDateTime updatedAt; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__insert_department_records.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO departments (department_code, department_name, department_description, created_at, updated_at) 2 | VALUES 3 | ('HR', 'Human Resources', 'Handles all matters related to employees', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), 4 | ('IT', 'Information Technology', 'Manages and maintains IT infrastructure', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), 5 | ('MK', 'Marketing', 'Promotes and markets company products', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), 6 | ('SA', 'Sales', 'Handles client sales and services', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); 7 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/enums/ReportLanguage.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.enums; 2 | 3 | import lombok.Getter; 4 | import java.util.Locale; 5 | 6 | @Getter 7 | public enum ReportLanguage { 8 | EN("en", Locale.ENGLISH), // English 9 | ES("es", new Locale("es")), // Spanish 10 | DE("de", Locale.GERMAN), // German 11 | EL("el", new Locale("el")); // Greek 12 | 13 | private final String code; 14 | private final Locale locale; 15 | 16 | ReportLanguage(String code, Locale locale) { 17 | this.code = code; 18 | this.locale = locale; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/DepartmentService.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service; 2 | 3 | import com.ainigma100.departmentapi.dto.DepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.DepartmentSearchCriteriaDTO; 5 | import org.springframework.data.domain.Page; 6 | 7 | public interface DepartmentService { 8 | 9 | 10 | DepartmentDTO createDepartment(DepartmentDTO departmentDTO); 11 | 12 | Page getAllDepartmentsUsingPagination(DepartmentSearchCriteriaDTO departmentSearchCriteriaDTO); 13 | 14 | DepartmentDTO getDepartmentById(Long id); 15 | 16 | DepartmentDTO updateDepartment(DepartmentDTO departmentDTO, Long id); 17 | 18 | void deleteDepartment(Long id); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/application-testcontainers.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:tc:postgresql://localhost:5432/mydb 4 | username: postgres 5 | password: pass 6 | flyway: 7 | enabled: true 8 | baseline-on-migrate: true 9 | locations: classpath:db/migration 10 | jpa: 11 | properties: 12 | hibernate: 13 | dialect: org.hibernate.dialect.PostgreSQLDialect 14 | hibernate: 15 | ddl-auto: validate 16 | mail: 17 | host: localhost 18 | port: 1025 19 | protocol: smtp 20 | properties: 21 | mail: 22 | smtp: 23 | auth: false 24 | starttls: 25 | enable: false 26 | 27 | # Bucket4j to handle rate limit 28 | rate-limiting: 29 | max-requests: 10 30 | refill-duration: 1 31 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/email/EmailRequest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.email; 2 | 3 | import jakarta.activation.DataSource; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class EmailRequest { 17 | 18 | private String from; 19 | private List toRecipients; 20 | private List ccRecipients; 21 | private String subject; 22 | private String emailBody; 23 | private Map attachments; 24 | private Map dynamicVariables; 25 | private Map imagePaths; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/ReportService.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service; 2 | 3 | import com.ainigma100.departmentapi.dto.FileDTO; 4 | import com.ainigma100.departmentapi.enums.ReportLanguage; 5 | import net.sf.jasperreports.engine.JRException; 6 | 7 | import java.io.IOException; 8 | 9 | public interface ReportService { 10 | 11 | FileDTO generateDepartmentsExcelReport() throws JRException; 12 | 13 | FileDTO generateEmployeesExcelReport() throws JRException; 14 | 15 | FileDTO generatePdfFullReport(ReportLanguage language) throws JRException; 16 | 17 | FileDTO generateAndZipReports() throws JRException, IOException; 18 | 19 | FileDTO generateMultiSheetExcelReport() throws JRException; 20 | 21 | FileDTO generateCombinedPdfReport(ReportLanguage language) throws JRException; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | report.title=Department API - Full Report 2 | report.extraction-date=Extraction Date: 3 | 4 | report.total-departments=Total Departments: 5 | report.total-employees=Total Employees: 6 | 7 | 8 | # Departments table 9 | report.dep-header-1=ID 10 | report.dep-header-2=Department Code 11 | report.dep-header-3=Department Name 12 | report.dep-header-4=Department Description 13 | report.dep-header-5=Total Employees 14 | report.dep-header-6=Created At 15 | report.dep-header-7=Updated At 16 | 17 | 18 | # Employees table 19 | report.emp-header-1=First Name 20 | report.emp-header-2=Last Name 21 | report.emp-header-3=Email 22 | report.emp-header-4=Department Name 23 | report.emp-header-5=Created At 24 | report.emp-header-6=Updated At 25 | report.emp-header-7=Salary 26 | # table summary 27 | report.emp-total=Total Salary 28 | -------------------------------------------------------------------------------- /src/main/resources/messages_el.properties: -------------------------------------------------------------------------------- 1 | report.title=Τμήμα API - Πλήρης Αναφορά 2 | report.extraction-date=Ημερ. Εξαγωγής: 3 | 4 | report.total-departments=Σύνολο Τμημάτων: 5 | report.total-employees=Σύνολο Εργαζομένων: 6 | 7 | 8 | # Departments table 9 | report.dep-header-1=ID 10 | report.dep-header-2=Κωδικός Τμήματος 11 | report.dep-header-3=Όνομα Τμήματος 12 | report.dep-header-4=Περιγραφή Τμήματος 13 | report.dep-header-5=Σύνολο Εργαζομένων 14 | report.dep-header-6=Δημιουργήθηκε 15 | report.dep-header-7=Ενημερώθηκε 16 | 17 | 18 | # Employees table 19 | report.emp-header-1=Όνομα 20 | report.emp-header-2=Επώνυμο 21 | report.emp-header-3=Ηλεκτρονικό Ταχυδρομείο 22 | report.emp-header-4=Όνομα Τμήματος 23 | report.emp-header-5=Δημιουργήθηκε 24 | report.emp-header-6=Ενημερώθηκε 25 | report.emp-header-7=Μισθός 26 | # table summary 27 | report.emp-total=Σύνολο Μισθών 28 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/EmployeeAndDepartmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import lombok.*; 4 | 5 | import java.math.BigDecimal; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class EmployeeAndDepartmentDTO { 12 | 13 | private String id; 14 | private String firstName; 15 | private String lastName; 16 | private String email; 17 | private BigDecimal salary; 18 | private DepartmentDTO department; 19 | 20 | 21 | @Getter 22 | @Setter 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | @Builder 26 | public static class DepartmentDTO { 27 | 28 | private Long id; 29 | private String departmentCode; 30 | private String departmentName; 31 | private String departmentDescription; 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/messages_de.properties: -------------------------------------------------------------------------------- 1 | report.title=Abteilung API - Gesamtbericht 2 | report.extraction-date=Extraktionsdatum: 3 | 4 | report.total-departments=Gesamtanzahl der Abteilungen: 5 | report.total-employees=Gesamtanzahl der Mitarbeiter: 6 | 7 | 8 | # Departments table 9 | report.dep-header-1=ID 10 | report.dep-header-2=Abteilungscode 11 | report.dep-header-3=Abteilungsname 12 | report.dep-header-4=Abteilungsbeschreibung 13 | report.dep-header-5=Gesamtanzahl der Mitarbeiter 14 | report.dep-header-6=Erstellt am 15 | report.dep-header-7=Aktualisiert am 16 | 17 | 18 | # Employees table 19 | report.emp-header-1=Vorname 20 | report.emp-header-2=Nachname 21 | report.emp-header-3=E-Mail 22 | report.emp-header-4=Abteilungsname 23 | report.emp-header-5=Erstellt am 24 | report.emp-header-6=Aktualisiert am 25 | report.emp-header-7=Gehalt 26 | # table summary 27 | report.emp-total=Gesamtgehalt 28 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__init_schemas.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS departments 2 | ( 3 | id BIGSERIAL PRIMARY KEY, 4 | department_code VARCHAR(255) NOT NULL UNIQUE, 5 | department_name VARCHAR(255) NOT NULL UNIQUE, 6 | department_description VARCHAR(255) NOT NULL, 7 | created_at TIMESTAMP WITHOUT TIME ZONE, 8 | updated_at TIMESTAMP WITHOUT TIME ZONE 9 | ); 10 | 11 | CREATE TABLE IF NOT EXISTS employees 12 | ( 13 | id VARCHAR(255) PRIMARY KEY, 14 | first_name VARCHAR(255), 15 | last_name VARCHAR(255), 16 | email VARCHAR(255) NOT NULL UNIQUE, 17 | salary DECIMAL(10, 2), 18 | created_at TIMESTAMP WITHOUT TIME ZONE, 19 | updated_at TIMESTAMP WITHOUT TIME ZONE, 20 | department_id BIGINT NOT NULL, 21 | FOREIGN KEY (department_id) REFERENCES departments (id) 22 | ); 23 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/DepartmentRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import jakarta.validation.constraints.NotEmpty; 4 | import jakarta.validation.constraints.Size; 5 | import lombok.*; 6 | 7 | @Getter 8 | @Setter 9 | @ToString 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DepartmentRequestDTO { 13 | 14 | 15 | @NotEmpty(message = "Department code should not be null or empty") 16 | @Size(min = 2, message = "Department code should have at least 2 characters") 17 | private String departmentCode; 18 | 19 | @NotEmpty(message = "Department name should not be null or empty") 20 | @Size(min = 2, message = "Department name should have at least 2 characters") 21 | private String departmentName; 22 | 23 | @NotEmpty(message = "Department description should not be null or empty") 24 | @Size(min = 10, message = "Department description should have at least 10 characters") 25 | private String departmentDescription; 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/EmployeeService.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service; 2 | 3 | import com.ainigma100.departmentapi.dto.EmployeeAndDepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.EmployeeDTO; 5 | import com.ainigma100.departmentapi.dto.EmployeeSearchCriteriaDTO; 6 | import org.springframework.data.domain.Page; 7 | 8 | import java.util.List; 9 | 10 | public interface EmployeeService { 11 | 12 | EmployeeDTO createEmployee(Long departmentId, EmployeeDTO employeeDTO); 13 | 14 | Page getAllEmployeesUsingPagination(EmployeeSearchCriteriaDTO employeeSearchCriteriaDTO); 15 | 16 | List getEmployeesByDepartmentId(Long departmentId); 17 | 18 | EmployeeDTO getEmployeeById(Long departmentId, String employeeId); 19 | 20 | EmployeeDTO updateEmployeeById(Long departmentId, String employeeId, EmployeeDTO employeeDTO); 21 | 22 | void deleteEmployee(Long departmentId, String employeeId); 23 | 24 | EmployeeAndDepartmentDTO getEmployeeAndDepartmentByEmployeeEmail(String email); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/EmployeeSearchCriteriaDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import com.ainigma100.departmentapi.utils.SortItem; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Positive; 7 | import jakarta.validation.constraints.PositiveOrZero; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | import java.util.List; 12 | 13 | @Setter 14 | @Getter 15 | public class EmployeeSearchCriteriaDTO { 16 | 17 | private String firstName; 18 | private String lastName; 19 | 20 | private String email; 21 | 22 | @Schema(example = "0") 23 | @NotNull(message = "page cannot be null") 24 | @PositiveOrZero(message = "page must be a zero or a positive number") 25 | private Integer page; 26 | 27 | @Schema(example = "10") 28 | @NotNull(message = "size cannot be null") 29 | @Positive(message = "size must be a positive number") 30 | private Integer size; 31 | 32 | private List sortList; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/EmployeeRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.*; 8 | 9 | import java.math.BigDecimal; 10 | 11 | @Getter 12 | @Setter 13 | @ToString 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class EmployeeRequestDTO { 17 | 18 | 19 | @NotEmpty(message = "First name should not be null or empty") 20 | @Size(min = 2, message = "First name should have at least 2 characters") 21 | private String firstName; 22 | 23 | @NotEmpty(message = "Last name should not be null or empty") 24 | @Size(min = 2, message = "Last name should have at least 2 characters") 25 | private String lastName; 26 | 27 | @NotEmpty(message = "Email should not be null or empty") 28 | @Email(regexp = "[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,3}", 29 | flags = Pattern.Flag.CASE_INSENSITIVE) 30 | private String email; 31 | 32 | 33 | private BigDecimal salary; 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/dto/DepartmentSearchCriteriaDTO.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.dto; 2 | 3 | import com.ainigma100.departmentapi.utils.SortItem; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Positive; 7 | import jakarta.validation.constraints.PositiveOrZero; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.Setter; 11 | 12 | import java.util.List; 13 | 14 | @Setter 15 | @Getter 16 | @RequiredArgsConstructor 17 | public class DepartmentSearchCriteriaDTO { 18 | 19 | private String departmentCode; 20 | 21 | private String departmentName; 22 | 23 | private String departmentDescription; 24 | 25 | 26 | @Schema(example = "0") 27 | @NotNull(message = "page cannot be null") 28 | @PositiveOrZero(message = "page must be a zero or a positive number") 29 | private Integer page; 30 | 31 | @Schema(example = "10") 32 | @NotNull(message = "size cannot be null") 33 | @Positive(message = "size must be a positive number") 34 | private Integer size; 35 | 36 | private List sortList; 37 | 38 | public DepartmentSearchCriteriaDTO(int page, int size, List sortList) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/jasperreport/SimpleReportFiller.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.jasperreport; 2 | 3 | import net.sf.jasperreports.engine.*; 4 | import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.io.InputStream; 8 | import java.util.Map; 9 | 10 | @Component 11 | public class SimpleReportFiller { 12 | 13 | 14 | public JasperPrint prepareReport(String templateFileName, Map parameters, JRBeanCollectionDataSource dataSource) throws JRException { 15 | 16 | JasperReport report = compileReport(templateFileName); 17 | return fillReport(report, parameters, dataSource); 18 | 19 | } 20 | 21 | public JasperReport compileReport(String templateFileName) throws JRException { 22 | 23 | InputStream reportStream = getClass().getResourceAsStream("/".concat(templateFileName).concat(".jrxml")); 24 | return JasperCompileManager.compileReport(reportStream); 25 | 26 | } 27 | 28 | public JasperPrint fillReport(JasperReport report, Map parameters, JRBeanCollectionDataSource dataSource) throws JRException { 29 | 30 | return JasperFillManager.fillReport(report, parameters, dataSource); 31 | 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/config/InternationalizationConfig.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 6 | import org.springframework.web.servlet.LocaleResolver; 7 | import org.springframework.web.servlet.i18n.SessionLocaleResolver; 8 | 9 | import java.util.Locale; 10 | 11 | @Configuration 12 | public class InternationalizationConfig { 13 | 14 | @Bean 15 | public LocaleResolver localeResolver(){ 16 | 17 | SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver(); 18 | sessionLocaleResolver.setDefaultLocale(Locale.US); 19 | 20 | return sessionLocaleResolver; 21 | } 22 | 23 | 24 | @Bean 25 | public ReloadableResourceBundleMessageSource messageSource() { 26 | 27 | ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); 28 | // specify the base name of the Resource Bundle 29 | messageSource.setBasename("classpath:messages"); 30 | messageSource.setCacheSeconds(2500000); 31 | messageSource.setDefaultEncoding("UTF-8"); 32 | messageSource.setUseCodeAsDefaultMessage(true); 33 | 34 | return messageSource; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/entity/Employee.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import org.hibernate.annotations.CreationTimestamp; 10 | import org.hibernate.annotations.UpdateTimestamp; 11 | import org.hibernate.annotations.UuidGenerator; 12 | 13 | import java.math.BigDecimal; 14 | import java.time.LocalDateTime; 15 | 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @Entity 21 | @Table(name = "employees") 22 | public class Employee { 23 | 24 | 25 | @Id 26 | @GeneratedValue(generator = "uuid2") 27 | @UuidGenerator 28 | private String id; 29 | private String firstName; 30 | private String lastName; 31 | 32 | @Column(nullable = false, unique = true) 33 | private String email; 34 | 35 | private BigDecimal salary; 36 | 37 | @CreationTimestamp 38 | @Column(nullable = false, updatable = false) 39 | private LocalDateTime createdAt; 40 | 41 | @UpdateTimestamp 42 | private LocalDateTime updatedAt; 43 | 44 | @JsonIgnoreProperties("employees") 45 | @ManyToOne(fetch = FetchType.LAZY) 46 | @JoinColumn(name = "department_id", nullable = false) 47 | private Department department; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/repository/DepartmentRepository.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.repository; 2 | 3 | import com.ainigma100.departmentapi.dto.DepartmentSearchCriteriaDTO; 4 | import com.ainigma100.departmentapi.entity.Department; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | 11 | public interface DepartmentRepository extends JpaRepository { 12 | 13 | Department findByDepartmentCode(String departmentCode); 14 | 15 | 16 | @Query(value = """ 17 | select dep from Department dep 18 | where ( :#{#criteria.departmentCode} IS NULL OR LOWER(dep.departmentCode) LIKE LOWER( CONCAT(:#{#criteria.departmentCode}, '%') ) ) 19 | and ( :#{#criteria.departmentName} IS NULL OR LOWER(dep.departmentName) LIKE LOWER( CONCAT(:#{#criteria.departmentName}, '%') ) ) 20 | and ( :#{#criteria.departmentDescription} IS NULL OR LOWER(dep.departmentDescription) LIKE LOWER( CONCAT('%', :#{#criteria.departmentDescription}, '%') ) ) 21 | """) 22 | Page getAllDepartmentsUsingPagination( 23 | @Param("criteria") DepartmentSearchCriteriaDTO departmentSearchCriteriaDTO, 24 | Pageable pageable); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/filter/FiltersConfig.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.filter; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @AllArgsConstructor 9 | @Configuration 10 | public class FiltersConfig { 11 | 12 | private final RateLimitingFilter rateLimitingFilter; 13 | private final LoggingFilter loggingFilter; 14 | 15 | 16 | 17 | @Bean 18 | public FilterRegistrationBean loggingFilterBean() { 19 | 20 | final FilterRegistrationBean filterBean = new FilterRegistrationBean<>(); 21 | filterBean.setFilter(loggingFilter); 22 | filterBean.addUrlPatterns("/api/v1/*"); 23 | // Lower values have higher priority 24 | filterBean.setOrder(2); 25 | 26 | return filterBean; 27 | } 28 | 29 | @Bean 30 | public FilterRegistrationBean rateLimitingFilterFilterRegistrationBean() { 31 | 32 | final FilterRegistrationBean filterBean = new FilterRegistrationBean<>(); 33 | filterBean.setFilter(rateLimitingFilter); 34 | filterBean.addUrlPatterns("/api/v1/*"); 35 | // Lower values have higher priority 36 | filterBean.setOrder(1); 37 | 38 | return filterBean; 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/email/MailHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.email; 2 | 3 | import jakarta.mail.MessagingException; 4 | import jakarta.mail.Session; 5 | import jakarta.mail.Transport; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.boot.actuate.health.Health; 8 | import org.springframework.boot.actuate.health.HealthIndicator; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.stereotype.Component; 11 | 12 | @AllArgsConstructor 13 | @Component 14 | public class MailHealthIndicator implements HealthIndicator { 15 | 16 | private final JavaMailSender mailSender; 17 | 18 | 19 | 20 | @Override 21 | public Health health() { 22 | try { 23 | testMailServerConnection(); 24 | return Health.up().build(); 25 | } catch (Exception e) { 26 | return Health.down().withDetail("error", e.getMessage()).build(); 27 | } 28 | } 29 | 30 | private void testMailServerConnection() throws MessagingException { 31 | 32 | Session session = ((org.springframework.mail.javamail.JavaMailSenderImpl) mailSender).getSession(); 33 | Transport transport = session.getTransport("smtp"); 34 | 35 | try { 36 | transport.connect(); 37 | } finally { 38 | transport.close(); // Ensure that the transport is closed even if an exception occurs. 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/mapper/DepartmentMapper.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.mapper; 2 | 3 | import com.ainigma100.departmentapi.dto.DepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.DepartmentReportDTO; 5 | import com.ainigma100.departmentapi.dto.DepartmentRequestDTO; 6 | import com.ainigma100.departmentapi.entity.Department; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | 10 | import java.util.List; 11 | 12 | @Mapper(componentModel = "spring", uses = {EmployeeMapper.class}) 13 | public interface DepartmentMapper { 14 | 15 | Department departmentDtoToDepartment(DepartmentDTO departmentDTO); 16 | DepartmentDTO departmentToDepartmentDto(Department department); 17 | 18 | List departmentDtoToDepartment(List departmentDTOList); 19 | List departmentToDepartmentDto(List departmentList); 20 | 21 | DepartmentDTO departmentRequestDTOToDepartmentDTO(DepartmentRequestDTO departmentRequestDTO); 22 | 23 | @Mapping(target = "totalEmployees", expression = "java(department.getEmployees() != null ? department.getEmployees().size() : 0)") 24 | DepartmentReportDTO departmentToDepartmentReportDto(Department department); 25 | 26 | @Mapping(target = "totalEmployees", expression = "java(departmentList.getEmployees() != null ? departmentList.getEmployees().size() : 0)") 27 | List departmentToDepartmentReportDto(List departmentList); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/entity/Department.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import jakarta.persistence.*; 5 | import lombok.*; 6 | import org.hibernate.annotations.CreationTimestamp; 7 | import org.hibernate.annotations.UpdateTimestamp; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | @Getter 14 | @Setter 15 | @ToString 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Entity 19 | @Table(name = "departments") 20 | public class Department { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | @Column(name = "department_code", nullable = false, unique = true) 27 | private String departmentCode; 28 | 29 | @Column(name = "department_name", nullable = false, unique = true) 30 | private String departmentName; 31 | 32 | @Column(name = "department_description", nullable = false) 33 | private String departmentDescription; 34 | 35 | 36 | @CreationTimestamp 37 | @Column(nullable = false, updatable = false) 38 | private LocalDateTime createdAt; 39 | 40 | @UpdateTimestamp 41 | private LocalDateTime updatedAt; 42 | 43 | // order the elements of the set 44 | // @OrderBy("id ASC") 45 | @JsonIgnoreProperties("department") 46 | @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true) 47 | private Set employees = new HashSet<>(); 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V3__insert_employee_records.sql: -------------------------------------------------------------------------------- 1 | -- Inserting employees for Human Resources 2 | INSERT INTO employees (id, first_name, last_name, email, salary, department_id, created_at, updated_at) 3 | VALUES 4 | ('1a2b3c4d-5e6f-7g8h-9i0j-abcdefgh1234', 'Alice', 'Johnson', 'alice.johnson@email.com', 50000.00, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), 5 | ('2a3b4c5d-6e7f-8g9h-0i1j-abcdefgh5678', 'Bob', 'Smith', 'bob.smith@email.com', 55000.00, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); 6 | 7 | -- Inserting employees for Information Technology 8 | INSERT INTO employees (id, first_name, last_name, email, salary, department_id, created_at, updated_at) 9 | VALUES 10 | ('3a4b5c6d-7e8f-9g0h-1i2j-abcdefgh9101', 'Charlie', 'Brown', 'charlie.brown@email.com', 60000.00, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), 11 | ('4a5b6c7d-8e9f-0g1h-2i3j-abcdefgh1121', 'David', 'Lee', 'david.lee@email.com', 65000.00, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); 12 | 13 | -- Inserting employees for Marketing 14 | INSERT INTO employees (id, first_name, last_name, email, salary, department_id, created_at, updated_at) 15 | VALUES 16 | ('5a6b7c8d-9e0f-1g2h-3i4j-abcdefgh3141', 'Eve', 'Adams', 'eve.adams@email.com', 52000.00, 3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); 17 | 18 | -- Inserting employees for Sales 19 | INSERT INTO employees (id, first_name, last_name, email, salary, department_id, created_at, updated_at) 20 | VALUES 21 | ('6a7b8c9d-0e1f-2g3h-4i5j-abcdefgh5161', 'Frank', 'White', 'frank.white@email.com', 57000.00, 4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); 22 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/mapper/EmployeeMapper.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.mapper; 2 | 3 | import com.ainigma100.departmentapi.dto.EmployeeAndDepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.EmployeeDTO; 5 | import com.ainigma100.departmentapi.dto.EmployeeReportDTO; 6 | import com.ainigma100.departmentapi.dto.EmployeeRequestDTO; 7 | import com.ainigma100.departmentapi.entity.Employee; 8 | import org.mapstruct.Mapper; 9 | import org.mapstruct.Mapping; 10 | 11 | import java.util.List; 12 | 13 | @Mapper(componentModel = "spring") 14 | public interface EmployeeMapper { 15 | 16 | 17 | Employee employeeDtoToEmployee(EmployeeDTO employeeDTO); 18 | EmployeeDTO employeeToEmployeeDto(Employee employee); 19 | 20 | List employeeDtoToEmployee(List employeeDTOList); 21 | List employeeToEmployeeDto(List employeeList); 22 | 23 | EmployeeDTO employeeRequestDTOToEmployeeDTO(EmployeeRequestDTO employeeRequestDTO); 24 | 25 | @Mapping(target = "departmentName", expression = "java(employee.getDepartment() != null ? employee.getDepartment().getDepartmentName() : null)") 26 | EmployeeReportDTO employeeToEmployeeReportDto(Employee employee); 27 | 28 | @Mapping(target = "departmentName", expression = "java(employeeList.getDepartment() != null ? employeeList.getDepartment().getDepartmentName() : null)") 29 | List employeeToEmployeeReportDto(List employeeList); 30 | 31 | EmployeeAndDepartmentDTO employeeToEmployeeAndDepartmentDto(Employee employee); 32 | 33 | List employeeToEmployeeAndDepartmentDto(List employeeList); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/repository/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.repository; 2 | 3 | import com.ainigma100.departmentapi.dto.EmployeeSearchCriteriaDTO; 4 | import com.ainigma100.departmentapi.entity.Employee; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.EntityGraph; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | 12 | import java.util.List; 13 | 14 | public interface EmployeeRepository extends JpaRepository { 15 | 16 | List findByDepartmentId(Long departmentId); 17 | 18 | @Query(value = """ 19 | select emp from Employee emp 20 | where ( :#{#criteria.firstName} IS NULL OR LOWER(emp.firstName) LIKE LOWER( CONCAT(:#{#criteria.firstName}, '%') ) ) 21 | and ( :#{#criteria.lastName} IS NULL OR LOWER(emp.lastName) LIKE LOWER( CONCAT(:#{#criteria.lastName}, '%') ) ) 22 | and ( :#{#criteria.email} IS NULL OR LOWER(emp.email) LIKE LOWER( CONCAT('%', :#{#criteria.email}, '%') ) ) 23 | """) 24 | Page getAllEmployeesUsingPagination( 25 | @Param("criteria") EmployeeSearchCriteriaDTO employeeSearchCriteriaDTO, 26 | Pageable pageable); 27 | 28 | @EntityGraph(attributePaths = "department") 29 | @Query(value = """ 30 | select emp from Employee emp 31 | where emp.email = :email 32 | """) 33 | Employee getEmployeeAndDepartmentByEmployeeEmail(@Param("email") String email); 34 | 35 | Employee findByEmail(String email); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/utils/TestHelper.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.core.io.ClassPathResource; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | 12 | 13 | public class TestHelper { 14 | 15 | public static String extractJsonPropertyFromFile(String filePath, String propertyName) throws IOException { 16 | 17 | String jsonContent = loadJsonFileContentAsString(filePath); 18 | return extractJsonProperty(jsonContent, propertyName); 19 | 20 | } 21 | 22 | 23 | private static String loadJsonFileContentAsString(String path) throws IOException { 24 | 25 | StringBuilder content = new StringBuilder(); 26 | 27 | try (InputStream inputStream = new ClassPathResource(path).getInputStream(); 28 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { 29 | String line; 30 | 31 | while ((line = reader.readLine()) != null) { 32 | content.append(line); 33 | } 34 | } 35 | 36 | return content.toString(); 37 | } 38 | 39 | private static String extractJsonProperty(String json, String propertyName) throws IOException { 40 | 41 | ObjectMapper objectMapper = new ObjectMapper(); 42 | JsonNode jsonNode = objectMapper.readTree(json); 43 | JsonNode propertyNode = jsonNode.get(propertyName); 44 | 45 | if (propertyNode != null) { 46 | return propertyNode.asText(); 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | servlet: 4 | context-path: '/@project.name@' 5 | shutdown: graceful 6 | 7 | spring: 8 | lifecycle: 9 | timeout-per-shutdown-phase: 25s 10 | application: 11 | name: '@project.name@' 12 | flyway: 13 | enabled: true 14 | validate-on-migrate: true 15 | locations: classpath:db/migration 16 | datasource: 17 | url: jdbc:postgresql://localhost:5432/mydb 18 | username: postgres 19 | password: pass 20 | jpa: 21 | show-sql: false 22 | hibernate: 23 | ddl-auto: validate 24 | properties: 25 | hibernate: 26 | # format_sql: true 27 | dialect: org.hibernate.dialect.PostgreSQLDialect 28 | ddl-auto: validate 29 | output: 30 | ansi: 31 | enabled: always 32 | # MailHog provides a local SMTP server for capturing and viewing emails during local development/testing. 33 | mail: 34 | host: ${EMAIL_HOST:localhost} 35 | port: 1025 36 | protocol: smtp 37 | properties: 38 | mail: 39 | smtp: 40 | auth: false 41 | starttls: 42 | enable: false 43 | 44 | # You can also use Mailtrap which is an Email Delivery Platform. You have to create an account https://mailtrap.io/ 45 | # mail: 46 | # host: smtp.mailtrap.io 47 | # port: 25 48 | # protocol: smtp 49 | # properties: 50 | # mail: 51 | # smtp: 52 | # auth: true 53 | # starttls: 54 | # enable: true 55 | # username: 56 | # password: 57 | 58 | 59 | springdoc: 60 | swagger-ui: 61 | path: /ui 62 | title: Department API 63 | version: '@springdoc-openapi-starter-webmvc-ui.version@' 64 | openapi: 65 | output: 66 | file: 'openapi-@project.name@.json' 67 | 68 | # Bucket4j to handle rate limit 69 | rate-limiting: 70 | max-requests: 10 71 | refill-duration: 1 72 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/controller/EmailController.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.dto.APIResponse; 4 | import com.ainigma100.departmentapi.enums.Status; 5 | import com.ainigma100.departmentapi.service.EmailService; 6 | import lombok.AllArgsConstructor; 7 | import net.sf.jasperreports.engine.JRException; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @AllArgsConstructor 15 | @RequestMapping("/api/v1/emails") 16 | @RestController 17 | public class EmailController { 18 | 19 | private final EmailService emailService; 20 | 21 | 22 | @GetMapping 23 | public ResponseEntity> sendEmailWithoutAttachment() { 24 | 25 | Boolean answer = emailService.sendEmailWithoutAttachment(); 26 | 27 | // Builder Design pattern 28 | APIResponse responseDTO = APIResponse 29 | .builder() 30 | .status(Status.SUCCESS.getValue()) 31 | .results(answer) 32 | .build(); 33 | 34 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 35 | } 36 | 37 | @GetMapping("/with-attachment") 38 | public ResponseEntity> sendEmailWithAttachment() throws JRException { 39 | 40 | Boolean answer = emailService.sendEmailWithAttachment(); 41 | 42 | // Builder Design pattern 43 | APIResponse responseDTO = APIResponse 44 | .builder() 45 | .status(Status.SUCCESS.getValue()) 46 | .results(answer) 47 | .build(); 48 | 49 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | postgresdb: 5 | image: postgres 6 | container_name: postgresdb 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: pass 10 | POSTGRES_DB: mydb 11 | ports: 12 | - "5432:5432" 13 | healthcheck: 14 | test: [ "CMD-SHELL", "pg_isready -d mydb -U postgres" ] 15 | interval: 5s 16 | timeout: 3s 17 | retries: 10 18 | networks: 19 | - springboot-postgres-net 20 | 21 | departmentapi: 22 | container_name: departmentapi 23 | build: 24 | context: ./ 25 | dockerfile: Dockerfile 26 | ports: 27 | - "8080:8080" 28 | depends_on: 29 | postgresdb: 30 | condition: service_healthy 31 | networks: 32 | - springboot-postgres-net 33 | environment: 34 | SPRING_DATASOURCE_URL: jdbc:postgresql://postgresdb:5432/mydb 35 | SPRING_DATASOURCE_USERNAME: postgres 36 | SPRING_DATASOURCE_PASSWORD: pass 37 | SPRING_JPA_HIBERNATE_DDL_AUTO: update 38 | EMAIL_HOST: mailhog 39 | restart: on-failure 40 | 41 | mailhog: 42 | container_name: mailhog 43 | image: mailhog/mailhog:v1.0.1 44 | ports: 45 | - "8025:8025" 46 | - "1025:1025" 47 | networks: 48 | - springboot-postgres-net 49 | restart: always 50 | 51 | 52 | # By default, SonarQube is using h2 database which is not recommended for production 53 | sonarqube: 54 | image: sonarqube:latest 55 | container_name: sonarqube 56 | ports: 57 | - "9000:9000" 58 | volumes: 59 | - sonarqube_data:/opt/sonarqube/data 60 | - sonarqube_extensions:/opt/sonarqube/extensions 61 | - sonarqube_logs:/opt/sonarqube/logs 62 | - sonarqube_temp:/opt/sonarqube/temp 63 | 64 | 65 | 66 | networks: 67 | sonar: 68 | springboot-postgres-net: 69 | driver: bridge 70 | volumes: 71 | sonarqube_data: 72 | sonarqube_extensions: 73 | sonarqube_logs: 74 | sonarqube_temp: 75 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/integration/AbstractContainerBaseTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.integration; 2 | 3 | import org.springframework.test.context.DynamicPropertyRegistry; 4 | import org.springframework.test.context.DynamicPropertySource; 5 | import org.testcontainers.containers.GenericContainer; 6 | import org.testcontainers.containers.PostgreSQLContainer; 7 | 8 | import java.util.function.Supplier; 9 | 10 | public abstract class AbstractContainerBaseTest { 11 | 12 | static final PostgreSQLContainer POSTGRE_SQL_CONTAINER; 13 | static final Supplier DATABASE_DRIVER = () -> "org.postgresql.Driver"; 14 | 15 | static final GenericContainer MAILHOG_CONTAINER = 16 | new GenericContainer<>("mailhog/mailhog") 17 | .withExposedPorts(1025, 8025); 18 | 19 | static { 20 | POSTGRE_SQL_CONTAINER = new PostgreSQLContainer("postgres:latest") 21 | .withDatabaseName("spring-boot-integration-test") 22 | .withUsername("postgres") 23 | .withPassword("pass"); 24 | 25 | POSTGRE_SQL_CONTAINER.start(); 26 | MAILHOG_CONTAINER.start(); 27 | } 28 | 29 | // Dynamically fetch the values from the container and add it to the application context 30 | @DynamicPropertySource 31 | public static void dynamicPropertySource(DynamicPropertyRegistry registry){ 32 | 33 | // Postgres 34 | registry.add("spring.datasource.url", POSTGRE_SQL_CONTAINER::getJdbcUrl); 35 | registry.add("spring.datasource.username", POSTGRE_SQL_CONTAINER::getUsername); 36 | registry.add("spring.datasource.password", POSTGRE_SQL_CONTAINER::getPassword); 37 | registry.add("spring.datasource.driver-class-name", DATABASE_DRIVER); 38 | 39 | // MailHog 40 | Integer mailHogSMTPPort = MAILHOG_CONTAINER.getMappedPort(1025); 41 | registry.add("spring.mail.host", MAILHOG_CONTAINER::getHost); 42 | registry.add("spring.mail.port", mailHogSMTPPort::toString); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/templates/email-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Email Template 6 | 56 | 57 | 58 |

Email Content

59 |

Hello ,

60 | 61 |
62 |

Thank you for your interest in our services. We are pleased to provide you with the following information:

63 |
64 | 65 |
66 |

You can check my GitHub repository here: Department API

67 |
68 | 69 | 70 | 71 |
72 |

Best regards,

73 |

Monkey D. Luffy

74 |

Pirate King

75 | Signature 76 |
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/filter/LoggingFilter.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.filter; 2 | 3 | import com.ainigma100.departmentapi.utils.Utils; 4 | import jakarta.servlet.*; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.io.IOException; 12 | 13 | /** 14 | * LoggingFilter is a servlet filter that logs HTTP requests and responses. 15 | * 16 | *

This filter logs the URL and method of incoming requests and the status 17 | * of outgoing responses. It excludes certain paths from logging, such as those 18 | * related to Actuator, Swagger, API documentation, favicon, and UI resources.

19 | * 20 | */ 21 | @Component 22 | @Slf4j 23 | @Order(2) 24 | public class LoggingFilter implements Filter { 25 | 26 | 27 | @Override 28 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 29 | throws IOException, ServletException { 30 | 31 | HttpServletRequest httpServletRequest = (HttpServletRequest) request; 32 | HttpServletResponse httpServletResponse = (HttpServletResponse) response; 33 | 34 | String clientIP = Utils.getClientIP(httpServletRequest); 35 | 36 | if ( this.shouldLogRequest(httpServletRequest) ) { 37 | log.info("Client IP: {}, Request URL: {}, Method: {}", clientIP, httpServletRequest.getRequestURL(), httpServletRequest.getMethod()); 38 | } 39 | 40 | // pre methods call stamps 41 | chain.doFilter(request, response); 42 | 43 | // post method calls stamps 44 | if ( this.shouldLogRequest(httpServletRequest) ) { 45 | log.info("Response status: {}", httpServletResponse.getStatus()); 46 | } 47 | 48 | } 49 | 50 | private boolean shouldLogRequest(HttpServletRequest request) { 51 | 52 | // (?i) enables case-insensitive matching, \b matched as whole words 53 | // reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions 54 | return !request.getServletPath().matches("(?i).*\\b(actuator|swagger|api-docs|favicon|ui|h2-console)\\b.*"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/filter/RateLimitingFilter.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.filter; 2 | 3 | import com.ainigma100.departmentapi.utils.Utils; 4 | import io.github.bucket4j.Bandwidth; 5 | import io.github.bucket4j.Bucket; 6 | import jakarta.servlet.*; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.core.annotation.Order; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.io.IOException; 16 | import java.time.Duration; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | @Component 21 | @Slf4j 22 | @Order(1) 23 | public class RateLimitingFilter implements Filter { 24 | 25 | @Value("${rate-limiting.max-requests}") 26 | private int maxNumberOfRequests; 27 | 28 | @Value("${rate-limiting.refill-duration}") 29 | private String refillDuration; 30 | 31 | 32 | private final Map bucketMap = new ConcurrentHashMap<>(); 33 | 34 | @Override 35 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 36 | throws IOException, ServletException { 37 | 38 | 39 | HttpServletRequest httpRequest = (HttpServletRequest) request; 40 | HttpServletResponse httpResponse = (HttpServletResponse) response; 41 | 42 | String clientIP = Utils.getClientIP(httpRequest); 43 | Bucket bucket = bucketMap.computeIfAbsent(clientIP, ip -> createNewBucket()); 44 | 45 | 46 | if (bucket.tryConsume(1)) { 47 | chain.doFilter(request, response); 48 | 49 | } else { 50 | log.warn("Rate limit exceeded for IP: {}. Blocking request.", clientIP); 51 | httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); 52 | httpResponse.getWriter().write("Rate limit exceeded. Please try again later."); 53 | } 54 | 55 | } 56 | 57 | public void clearBuckets() { 58 | bucketMap.clear(); 59 | } 60 | 61 | // reference here: https://bucket4j.com/ 62 | private Bucket createNewBucket() { 63 | 64 | long duration = Long.parseLong(refillDuration); 65 | 66 | Bandwidth limit = Bandwidth.builder() 67 | .capacity(maxNumberOfRequests) 68 | .refillGreedy(maxNumberOfRequests, Duration.ofMinutes(duration)) 69 | .build(); 70 | 71 | return Bucket.builder() 72 | .addLimit(limit) 73 | .build(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/filter/ServerDetails.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.filter; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.boot.context.event.ApplicationReadyEvent; 7 | import org.springframework.context.event.EventListener; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.net.InetAddress; 12 | import java.net.UnknownHostException; 13 | import java.util.Optional; 14 | 15 | /** 16 | * Component for logging server details upon application startup. 17 | * 18 | *

This class listens for the ApplicationReadyEvent to log important server details such as 19 | * the protocol, host, port, context path, active profiles, and the URL for accessing Swagger UI.

20 | */ 21 | @AllArgsConstructor 22 | @Component 23 | public class ServerDetails { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(ServerDetails.class); 26 | 27 | 28 | private final Environment environment; 29 | 30 | 31 | @EventListener(ApplicationReadyEvent.class) 32 | public void logServerDetails() { 33 | 34 | String serverSslKeyStore = "server.ssl.key-store"; 35 | String serverPortKey = "server.port"; 36 | String serverServletContextPath = "server.servlet.context-path"; 37 | String springdocSwaggerUiPath = "springdoc.swagger-ui.path"; 38 | String defaultProfile = "default"; 39 | 40 | 41 | String protocol = Optional.ofNullable(environment.getProperty(serverSslKeyStore)).map(key -> "https").orElse("http"); 42 | String host = getServerIP(); 43 | String serverPort = Optional.ofNullable(environment.getProperty(serverPortKey)).orElse("8080"); 44 | String contextPath = Optional.ofNullable(environment.getProperty(serverServletContextPath)).orElse(""); 45 | String[] activeProfiles = Optional.of(environment.getActiveProfiles()).orElse(new String[0]); 46 | String activeProfile = (activeProfiles.length > 0) ? String.join(",", activeProfiles) : defaultProfile; 47 | String swaggerUI = Optional.ofNullable(environment.getProperty(springdocSwaggerUiPath)).orElse("/swagger-ui/index.html"); 48 | 49 | log.info( 50 | """ 51 | 52 | 53 | Access Swagger UI URL: {}://{}:{}{}{} 54 | Active Profile: {} 55 | """, 56 | protocol, host, serverPort, contextPath, swaggerUI, 57 | activeProfile 58 | ); 59 | } 60 | 61 | private String getServerIP() { 62 | try { 63 | return InetAddress.getLocalHost().getHostAddress(); 64 | } catch (UnknownHostException e) { 65 | log.error("Error resolving host address", e); 66 | return "unknown"; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/integration/EmailControllerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.integration; 2 | 3 | import com.ainigma100.departmentapi.enums.Status; 4 | import com.ainigma100.departmentapi.filter.RateLimitingFilter; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.Tag; 7 | import org.junit.jupiter.api.Test; 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.test.context.ActiveProfiles; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | import org.springframework.test.web.servlet.ResultActions; 14 | import org.testcontainers.junit.jupiter.Testcontainers; 15 | 16 | import static org.hamcrest.CoreMatchers.is; 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 21 | 22 | // Use random port for integration testing. the server will start on a random port 23 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 24 | @AutoConfigureMockMvc 25 | @Testcontainers(disabledWithoutDocker = true) 26 | @ActiveProfiles("testcontainers") 27 | @Tag("integration") 28 | class EmailControllerIntegrationTest extends AbstractContainerBaseTest { 29 | 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @Autowired 34 | private RateLimitingFilter rateLimitingFilter; 35 | 36 | 37 | @AfterEach 38 | void resetRateLimitBuckets() { 39 | rateLimitingFilter.clearBuckets(); 40 | } 41 | 42 | @Test 43 | void givenNoInput_whenSendEmailWithoutAttachment_thenReturnTrue() throws Exception { 44 | 45 | // given - precondition or setup 46 | 47 | // when - action or behaviour that we are going to test 48 | ResultActions response = mockMvc.perform(get("/api/v1/emails")); 49 | 50 | // then - verify the output 51 | response.andDo(print()) 52 | // verify the status code that is returned 53 | .andExpect(status().isOk()) 54 | // verify the actual returned value and the expected value 55 | // $ - root member of a JSON structure whether it is an object or array 56 | .andExpect(jsonPath("$.status", is(Status.SUCCESS.getValue()))); 57 | 58 | } 59 | 60 | @Test 61 | void givenNoInput_whenSendEmailWithAttachment_thenReturnTrue() throws Exception { 62 | 63 | // given - precondition or setup 64 | 65 | // when - action or behaviour that we are going to test 66 | ResultActions response = mockMvc.perform(get("/api/v1/emails/with-attachment")); 67 | 68 | // then - verify the output 69 | response.andDo( print() ) 70 | // verify the status code that is returned 71 | .andExpect( status().isOk() ) 72 | // verify the actual returned value and the expected value 73 | // $ - root member of a JSON structure whether it is an object or array 74 | .andExpect(jsonPath("$.status", is(Status.SUCCESS.getValue()))); 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/annotation/ExecutionTimeCalculator.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.annotation; 2 | 3 | import java.time.Instant; 4 | import java.time.LocalDateTime; 5 | import java.time.ZoneId; 6 | 7 | import org.aspectj.lang.ProceedingJoinPoint; 8 | import org.aspectj.lang.annotation.Around; 9 | import org.aspectj.lang.annotation.Aspect; 10 | import org.springframework.stereotype.Component; 11 | 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | @Slf4j 15 | @Aspect 16 | @Component 17 | public class ExecutionTimeCalculator { 18 | 19 | // \033[38;2;;;m #Select RGB foreground color 20 | // \033[48;2;;;m #Select RGB background color 21 | 22 | // more colors can be found here: 23 | // https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#256-colors 24 | public static final String ANSI_RESET = "\u001B[0m"; 25 | public static final String ANSI_GREEN = "\033[38;2;0;189;25m"; 26 | public static final String ANSI_YELLOW = "\033[38;2;255;255;0m"; 27 | public static final String ANSI_BLUE = "\u001B[38;5;33m"; 28 | public static final String ANSI_BLACK = "\u001B[38;5;234m"; 29 | public static final String ANSI_GREY = "\u001B[38;5;249m"; 30 | public static final String ANSI_RED = "\u001B[38;5;196m"; 31 | public static final String ANSI_ORANGE = "\u001B[38;5;208m"; 32 | 33 | 34 | // I have to put the whole path to my custom annotation class as a parameter of @annotation 35 | // @Around can perform custom behavior Before and After the method Invocation 36 | @Around("@annotation(com.ainigma100.departmentapi.utils.annotation.ExecutionTime)") 37 | public Object getExecutionTime(ProceedingJoinPoint proJoinPoint) throws Throwable { 38 | 39 | // get time when method execution starts 40 | long startTimeInMillis = System.currentTimeMillis(); 41 | 42 | LocalDateTime startLocalDateTime = Instant.ofEpochMilli(startTimeInMillis) 43 | .atZone(ZoneId.systemDefault()).toLocalDateTime(); 44 | 45 | // execute method 46 | Object obj = proJoinPoint.proceed(); 47 | 48 | 49 | // get time when method execution ends 50 | long endTimeInMillis = System.currentTimeMillis(); 51 | 52 | LocalDateTime endLocalDateTime = Instant.ofEpochMilli(endTimeInMillis) 53 | .atZone(ZoneId.systemDefault()).toLocalDateTime(); 54 | 55 | // get elapsed time in milliseconds 56 | long executionTimeInMilliseconds = (endTimeInMillis - startTimeInMillis); 57 | 58 | // milliseconds to minutes. 59 | long minutes = (executionTimeInMilliseconds / 1000) / 60; 60 | 61 | // milliseconds to seconds 62 | long seconds = (executionTimeInMilliseconds / 1000) % 60; 63 | 64 | // remaining milliseconds 65 | long milliseconds = executionTimeInMilliseconds % 1000; 66 | 67 | 68 | log.info("Method {} called at: {} and finished execution at: {}. The execution time was: {} minutes and {} seconds with {} milliseconds.", 69 | ANSI_YELLOW + proJoinPoint.getSignature().getName() + ANSI_RESET, 70 | ANSI_ORANGE + startLocalDateTime + ANSI_RESET, 71 | ANSI_ORANGE + endLocalDateTime + ANSI_RESET, 72 | ANSI_GREEN + minutes + ANSI_RESET, 73 | ANSI_GREEN + seconds + ANSI_RESET, 74 | ANSI_GREEN + milliseconds + ANSI_RESET); 75 | 76 | 77 | return obj; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/controller/EmailControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.enums.Status; 4 | import com.ainigma100.departmentapi.filter.RateLimitingFilter; 5 | import com.ainigma100.departmentapi.service.EmailService; 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.Tag; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 11 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | import org.springframework.test.web.servlet.ResultActions; 14 | 15 | import static org.hamcrest.CoreMatchers.is; 16 | import static org.mockito.BDDMockito.given; 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 21 | 22 | /* 23 | * @WebMvcTest annotation will load all the components required 24 | * to test the Controller layer. It will not load the service or repository layer components 25 | */ 26 | @WebMvcTest(EmailController.class) 27 | @Tag("unit") 28 | class EmailControllerTest { 29 | 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @MockitoBean 34 | private EmailService emailService; 35 | 36 | @Autowired 37 | private RateLimitingFilter rateLimitingFilter; 38 | 39 | 40 | @AfterEach 41 | void resetRateLimitBuckets() { 42 | rateLimitingFilter.clearBuckets(); 43 | } 44 | 45 | @Test 46 | void givenNoInput_whenSendEmailWithoutAttachment_thenReturnTrueIfMailWasSent() throws Exception { 47 | 48 | // given - precondition or setup 49 | given(emailService.sendEmailWithoutAttachment()).willReturn(true); 50 | 51 | // when - action or behaviour that we are going to test 52 | ResultActions response = mockMvc.perform(get("/api/v1/emails")); 53 | 54 | // then - verify the output 55 | response.andDo(print()) 56 | // verify the status code that is returned 57 | .andExpect(status().isOk()) 58 | // verify the actual returned value and the expected value 59 | // $ - root member of a JSON structure whether it is an object or array 60 | .andExpect(jsonPath("$.status", is(Status.SUCCESS.getValue()))); 61 | 62 | } 63 | 64 | 65 | @Test 66 | void givenNoInput_whenSendEmailWithAttachment_thenReturnTrueIfMailWasSent() throws Exception { 67 | 68 | // given - precondition or setup 69 | given(emailService.sendEmailWithAttachment()).willReturn(true); 70 | 71 | // when - action or behaviour that we are going to test 72 | ResultActions response = mockMvc.perform(get("/api/v1/emails/with-attachment")); 73 | 74 | // then - verify the output 75 | response.andDo(print()) 76 | // verify the status code that is returned 77 | .andExpect(status().isOk()) 78 | // verify the actual returned value and the expected value 79 | // $ - root member of a JSON structure whether it is an object or array 80 | .andExpect(jsonPath("$.status", is(Status.SUCCESS.getValue()))); 81 | 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/email/EmailSender.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.email; 2 | 3 | import jakarta.activation.DataSource; 4 | import jakarta.mail.MessagingException; 5 | import jakarta.mail.internet.MimeMessage; 6 | import lombok.AllArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.core.io.ClassPathResource; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.mail.javamail.MimeMessageHelper; 11 | import org.springframework.scheduling.annotation.Async; 12 | import org.springframework.stereotype.Component; 13 | import org.thymeleaf.TemplateEngine; 14 | import org.thymeleaf.context.Context; 15 | 16 | import java.util.Map; 17 | 18 | 19 | @Slf4j 20 | @AllArgsConstructor 21 | @Component 22 | public class EmailSender { 23 | 24 | private final JavaMailSender javaMailSender; 25 | private final TemplateEngine templateEngine; 26 | 27 | 28 | @Async 29 | public void sendEmail(EmailRequest emailRequest) { 30 | 31 | try { 32 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 33 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); 34 | 35 | // Set email properties 36 | helper.setFrom(emailRequest.getFrom()); 37 | helper.setSubject(emailRequest.getSubject()); 38 | helper.setTo(emailRequest.getToRecipients().toArray(new String[0])); 39 | 40 | final Context context = new Context(); 41 | // set variables contained inside the email body (thymeleaf html file) 42 | context.setVariables(emailRequest.getDynamicVariables()); 43 | 44 | // Set email body 45 | String processedEmailBody = templateEngine.process(emailRequest.getEmailBody(), context); 46 | helper.setText(processedEmailBody, true); 47 | 48 | // Attach dynamic images inline 49 | this.attachImagesInline(emailRequest, helper); 50 | // Attach dynamic attachments 51 | this.addAttachments(helper, emailRequest.getAttachments()); 52 | 53 | if (emailRequest.getCcRecipients() != null && !emailRequest.getCcRecipients().isEmpty()) { 54 | helper.setCc(emailRequest.getCcRecipients().toArray(new String[0])); 55 | } 56 | 57 | // Send email 58 | javaMailSender.send(mimeMessage); 59 | 60 | log.info("Email sent successfully"); 61 | 62 | } catch (MessagingException e) { 63 | log.error("Error sending email", e); 64 | } 65 | } 66 | 67 | 68 | private void attachImagesInline(EmailRequest emailRequest, MimeMessageHelper helper) throws MessagingException { 69 | 70 | if (emailRequest.getImagePaths() != null && !emailRequest.getImagePaths().isEmpty()) { 71 | 72 | for (Map.Entry entry : emailRequest.getImagePaths().entrySet()) { 73 | 74 | String variableName = entry.getKey(); 75 | String imagePath = entry.getValue(); 76 | ClassPathResource imageResource = new ClassPathResource(imagePath); 77 | 78 | if (imageResource.exists()) { 79 | helper.addInline(variableName, imageResource); 80 | } 81 | } 82 | } 83 | } 84 | 85 | 86 | private void addAttachments(MimeMessageHelper helper, Map attachments) throws MessagingException { 87 | 88 | if (attachments != null && !attachments.isEmpty()) { 89 | 90 | for (Map.Entry attachmentEntry : attachments.entrySet()) { 91 | 92 | String attachmentFilename = attachmentEntry.getKey(); 93 | DataSource attachmentDataSource = attachmentEntry.getValue(); 94 | 95 | helper.addAttachment(attachmentFilename, attachmentDataSource); 96 | } 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/resources/jrxml/excel/multiSheetExcelReport.jrxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/impl/EmailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service.impl; 2 | 3 | import com.ainigma100.departmentapi.dto.FileDTO; 4 | import com.ainigma100.departmentapi.enums.ReportLanguage; 5 | import com.ainigma100.departmentapi.service.EmailService; 6 | import com.ainigma100.departmentapi.service.ReportService; 7 | import com.ainigma100.departmentapi.utils.email.EmailRequest; 8 | import com.ainigma100.departmentapi.utils.email.EmailSender; 9 | import jakarta.activation.DataSource; 10 | import jakarta.mail.util.ByteArrayDataSource; 11 | import lombok.AllArgsConstructor; 12 | import net.sf.jasperreports.engine.JRException; 13 | import org.apache.tomcat.util.codec.binary.Base64; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | @AllArgsConstructor 21 | @Service 22 | public class EmailServiceImpl implements EmailService { 23 | 24 | private final EmailSender emailSender; 25 | private final ReportService reportService; 26 | 27 | 28 | @Override 29 | public Boolean sendEmailWithoutAttachment() { 30 | 31 | List toRecipients = List.of("jwick@gmail.com"); 32 | List ccRecipients = List.of("mpolo@gmail.com"); 33 | 34 | Map dynamicVariables = new HashMap<>(); 35 | dynamicVariables.put("recipientName", "John Wick"); 36 | dynamicVariables.put("githubRepoUrl", "https://github.com/pmoustopoulos/department-api"); 37 | 38 | Map imagePaths = new HashMap<>(); 39 | imagePaths.put("logo", "reportLogo/luffy.png"); 40 | 41 | EmailRequest emailRequest = new EmailRequest(); 42 | emailRequest.setFrom("lyffy@pirateking.com"); 43 | emailRequest.setEmailBody("email-template.html"); 44 | emailRequest.setAttachments(null); 45 | emailRequest.setSubject("Test Email"); 46 | emailRequest.setToRecipients(toRecipients); 47 | emailRequest.setCcRecipients(ccRecipients); 48 | emailRequest.setDynamicVariables(dynamicVariables); 49 | emailRequest.setImagePaths(imagePaths); 50 | 51 | 52 | emailSender.sendEmail(emailRequest); 53 | 54 | return true; 55 | } 56 | 57 | 58 | @Override 59 | public Boolean sendEmailWithAttachment() throws JRException { 60 | 61 | List toRecipients = List.of("jwick@gmail.com", "maria@gmail.com"); 62 | List ccRecipients = List.of("mpolo@gmail.com", "nick@gmail.com"); 63 | 64 | Map dynamicVariables = new HashMap<>(); 65 | dynamicVariables.put("recipientName", "John Wick"); 66 | dynamicVariables.put("githubRepoUrl", "https://github.com/pmoustopoulos/department-api"); 67 | 68 | Map imagePaths = new HashMap<>(); 69 | imagePaths.put("logo", "reportLogo/luffy.png"); 70 | 71 | // generate the first attachment 72 | FileDTO excelAttachment = reportService.generateDepartmentsExcelReport(); 73 | DataSource excelDataSource = new ByteArrayDataSource(excelAttachment.getFileContent(), "application/octet-stream"); 74 | 75 | // generate the second attachment 76 | FileDTO pdfAttachment = reportService.generatePdfFullReport(ReportLanguage.EN); 77 | DataSource pdfDataSource = new ByteArrayDataSource(pdfAttachment.getFileContent(), "application/octet-stream"); 78 | 79 | 80 | // Create a map of attachments 81 | Map attachments = new HashMap<>(); 82 | attachments.put(excelAttachment.getFileName(), excelDataSource); 83 | attachments.put(pdfAttachment.getFileName(), pdfDataSource); 84 | 85 | 86 | EmailRequest emailRequest = new EmailRequest(); 87 | emailRequest.setFrom("lyffy@pirateking.com"); 88 | emailRequest.setEmailBody("email-template.html"); 89 | emailRequest.setAttachments(attachments); 90 | emailRequest.setSubject("Test Email"); 91 | emailRequest.setToRecipients(toRecipients); 92 | emailRequest.setCcRecipients(ccRecipients); 93 | emailRequest.setDynamicVariables(dynamicVariables); 94 | emailRequest.setImagePaths(imagePaths); 95 | 96 | 97 | emailSender.sendEmail(emailRequest); 98 | 99 | return true; 100 | } 101 | 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/impl/DepartmentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service.impl; 2 | 3 | import com.ainigma100.departmentapi.dto.DepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.DepartmentSearchCriteriaDTO; 5 | import com.ainigma100.departmentapi.entity.Department; 6 | import com.ainigma100.departmentapi.mapper.DepartmentMapper; 7 | import com.ainigma100.departmentapi.repository.DepartmentRepository; 8 | import com.ainigma100.departmentapi.service.DepartmentService; 9 | import com.ainigma100.departmentapi.utils.SortItem; 10 | import com.ainigma100.departmentapi.utils.Utils; 11 | import jakarta.persistence.EntityExistsException; 12 | import jakarta.persistence.EntityNotFoundException; 13 | import lombok.AllArgsConstructor; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageImpl; 16 | import org.springframework.data.domain.Pageable; 17 | import org.springframework.stereotype.Service; 18 | 19 | import java.util.List; 20 | 21 | 22 | @AllArgsConstructor 23 | @Service 24 | public class DepartmentServiceImpl implements DepartmentService { 25 | 26 | private final DepartmentRepository departmentRepository; 27 | private final DepartmentMapper departmentMapper; 28 | 29 | 30 | @Override 31 | public DepartmentDTO createDepartment(DepartmentDTO departmentDTO) { 32 | 33 | Department recordFromDB = departmentRepository.findByDepartmentCode(departmentDTO.getDepartmentCode()); 34 | 35 | if (recordFromDB != null) { 36 | throw new EntityExistsException("Department with departmentCode '" + departmentDTO.getDepartmentCode() + "' already exists"); 37 | } 38 | 39 | Department recordToBeSaved = departmentMapper.departmentDtoToDepartment(departmentDTO); 40 | 41 | Department savedRecord = departmentRepository.save(recordToBeSaved); 42 | 43 | return departmentMapper.departmentToDepartmentDto(savedRecord); 44 | } 45 | 46 | @Override 47 | public Page getAllDepartmentsUsingPagination( 48 | DepartmentSearchCriteriaDTO departmentSearchCriteriaDTO) { 49 | 50 | 51 | Integer page = departmentSearchCriteriaDTO.getPage(); 52 | Integer size = departmentSearchCriteriaDTO.getSize(); 53 | List sortList = departmentSearchCriteriaDTO.getSortList(); 54 | 55 | // this pageable will be used for the pagination. 56 | Pageable pageable = Utils.createPageableBasedOnPageAndSizeAndSorting(sortList, page, size); 57 | 58 | Page recordsFromDb = departmentRepository.getAllDepartmentsUsingPagination(departmentSearchCriteriaDTO, pageable); 59 | 60 | List result = departmentMapper.departmentToDepartmentDto(recordsFromDb.getContent()); 61 | 62 | return new PageImpl<>(result, pageable, recordsFromDb.getTotalElements()); 63 | } 64 | 65 | @Override 66 | public DepartmentDTO getDepartmentById(Long id) { 67 | 68 | 69 | Department recordFromDB = departmentRepository.findById(id) 70 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + id + "' not found")); 71 | 72 | return departmentMapper.departmentToDepartmentDto(recordFromDB); 73 | } 74 | 75 | @Override 76 | public DepartmentDTO updateDepartment(DepartmentDTO departmentDTO, Long id) { 77 | 78 | 79 | Department recordFromDB = departmentRepository.findById(id) 80 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + id + "' not found")); 81 | 82 | // just to be safe that the object does not have another id 83 | departmentDTO.setId(id); 84 | 85 | 86 | Department recordToBeSaved = departmentMapper.departmentDtoToDepartment(departmentDTO); 87 | // I had to set again the employees otherwise I would lose the reference 88 | recordToBeSaved.setEmployees(recordFromDB.getEmployees()); 89 | 90 | Department savedRecord = departmentRepository.save(recordToBeSaved); 91 | 92 | return departmentMapper.departmentToDepartmentDto(savedRecord); 93 | } 94 | 95 | @Override 96 | public void deleteDepartment(Long id) { 97 | 98 | Department recordFromDB = departmentRepository.findById(id) 99 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + id + "' not found")); 100 | 101 | departmentRepository.delete(recordFromDB); 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 6 | import org.springframework.data.domain.PageRequest; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.domain.Sort; 9 | import org.springframework.data.domain.Sort.Order; 10 | 11 | import java.time.LocalDate; 12 | import java.time.format.DateTimeFormatter; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Locale; 16 | import java.util.Optional; 17 | import java.util.function.Supplier; 18 | 19 | @Slf4j 20 | public class Utils { 21 | 22 | // Private constructor to prevent instantiation 23 | private Utils() { 24 | throw new IllegalStateException("Utility class"); 25 | } 26 | 27 | public static Pageable createPageableBasedOnPageAndSizeAndSorting(List sortList, Integer page, Integer size) { 28 | 29 | List orders = new ArrayList<>(); 30 | 31 | if (sortList != null) { 32 | // iterate the SortList to see based on which attributes we are going to Order By the results. 33 | for(SortItem sortValue : sortList) { 34 | orders.add(new Order(sortValue.getDirection(), sortValue.getField())); 35 | } 36 | } 37 | 38 | 39 | return PageRequest.of( 40 | Optional.ofNullable(page).orElse(0), 41 | Optional.ofNullable(size).orElse(10), 42 | Sort.by(orders)); 43 | } 44 | 45 | public static String getCurrentDateAsString() { 46 | 47 | LocalDate localDate = LocalDate.now(); 48 | DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); 49 | 50 | return dateTimeFormatter.format(localDate); 51 | } 52 | 53 | 54 | public static void setEncodingForLocale(ReloadableResourceBundleMessageSource messageSource, Locale locale) { 55 | String encoding = determineEncoding(locale); 56 | messageSource.setDefaultEncoding(encoding); 57 | } 58 | 59 | private static String determineEncoding(Locale locale) { 60 | 61 | String language = locale.getLanguage().toLowerCase(); 62 | 63 | return switch (language) { 64 | case "es", "de" -> "ISO-8859-1"; 65 | default -> "UTF-8"; 66 | }; 67 | } 68 | 69 | 70 | /** 71 | * Retrieves a value from a Supplier or sets a default value if a NullPointerException occurs. 72 | * Usage example: 73 | * 74 | *
{@code
 75 |      * // Example 1: Retrieve a list or provide an empty list if null
 76 |      * List employeeList = Utils.retrieveValueOrSetDefault(() -> someSupplierMethod(), new ArrayList<>());
 77 |      *
 78 |      * // Example 2: Retrieve an Employee object or provide a default object if null
 79 |      * Employee emp = Utils.retrieveValueOrSetDefault(() -> anotherSupplierMethod(), new Employee());
 80 |      * }
81 | * 82 | * @param supplier the Supplier providing the value to retrieve 83 | * @param defaultValue the default value to return if a NullPointerException occurs 84 | * @return the retrieved value or the default value if a NullPointerException occurs 85 | * @param the type of the value 86 | */ 87 | public static T retrieveValueOrSetDefault(Supplier supplier, T defaultValue) { 88 | 89 | try { 90 | return supplier.get(); 91 | 92 | } catch (NullPointerException ex) { 93 | 94 | log.error("Error while retrieveValueOrSetDefault {}", ex.getMessage()); 95 | 96 | return defaultValue; 97 | } 98 | } 99 | 100 | public static String getClientIP(HttpServletRequest request) { 101 | 102 | String clientIP = request.getHeader("Client-IP"); 103 | 104 | if (clientIP == null || clientIP.isEmpty() || "unknown".equalsIgnoreCase(clientIP)) { 105 | clientIP = request.getHeader("X-Forwarded-For"); 106 | } 107 | 108 | if (clientIP == null || clientIP.isEmpty() || "unknown".equalsIgnoreCase(clientIP)) { 109 | clientIP = request.getHeader("X-Real-IP"); 110 | } 111 | 112 | if (clientIP == null || clientIP.isEmpty() || "unknown".equalsIgnoreCase(clientIP)) { 113 | clientIP = request.getRemoteAddr(); 114 | } 115 | 116 | return clientIP != null ? clientIP : "Unknown"; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/controller/DepartmentController.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.dto.APIResponse; 4 | import com.ainigma100.departmentapi.dto.DepartmentDTO; 5 | import com.ainigma100.departmentapi.dto.DepartmentRequestDTO; 6 | import com.ainigma100.departmentapi.dto.DepartmentSearchCriteriaDTO; 7 | import com.ainigma100.departmentapi.enums.Status; 8 | import com.ainigma100.departmentapi.mapper.DepartmentMapper; 9 | import com.ainigma100.departmentapi.service.DepartmentService; 10 | import io.swagger.v3.oas.annotations.Operation; 11 | import jakarta.validation.Valid; 12 | import lombok.AllArgsConstructor; 13 | import org.springframework.data.domain.Page; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | @AllArgsConstructor 19 | @RequestMapping("/api/v1/departments") 20 | @RestController 21 | public class DepartmentController { 22 | 23 | 24 | private final DepartmentService departmentService; 25 | private final DepartmentMapper departmentMapper; 26 | 27 | 28 | 29 | @Operation(summary = "Add a new department") 30 | @PostMapping 31 | public ResponseEntity> createDepartment( 32 | @Valid @RequestBody DepartmentRequestDTO departmentRequestDTO) { 33 | 34 | DepartmentDTO departmentDTO = departmentMapper.departmentRequestDTOToDepartmentDTO(departmentRequestDTO); 35 | 36 | DepartmentDTO result = departmentService.createDepartment(departmentDTO); 37 | 38 | // Builder Design pattern 39 | APIResponse responseDTO = APIResponse 40 | .builder() 41 | .status(Status.SUCCESS.getValue()) 42 | .results(result) 43 | .build(); 44 | 45 | 46 | return new ResponseEntity<>(responseDTO, HttpStatus.CREATED); 47 | } 48 | 49 | 50 | @Operation(summary = "Search all departments using pagination", 51 | description = "Returns a list of departments") 52 | @PostMapping("/search") 53 | public ResponseEntity>> getAllDepartmentsUsingPagination( 54 | @Valid @RequestBody DepartmentSearchCriteriaDTO departmentSearchCriteriaDTO) { 55 | 56 | Page result = departmentService.getAllDepartmentsUsingPagination(departmentSearchCriteriaDTO); 57 | 58 | // Builder Design pattern 59 | APIResponse> responseDTO = APIResponse 60 | .>builder() 61 | .status(Status.SUCCESS.getValue()) 62 | .results(result) 63 | .build(); 64 | 65 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 66 | } 67 | 68 | 69 | @Operation(summary = "Find department by ID", 70 | description = "Returns a single department") 71 | @GetMapping("/{id}") 72 | public ResponseEntity> getDepartmentById(@PathVariable("id") Long id) { 73 | 74 | DepartmentDTO result = departmentService.getDepartmentById(id); 75 | 76 | // Builder Design pattern 77 | APIResponse responseDTO = APIResponse 78 | .builder() 79 | .status(Status.SUCCESS.getValue()) 80 | .results(result) 81 | .build(); 82 | 83 | 84 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 85 | 86 | } 87 | 88 | 89 | @Operation(summary = "Update an existing department") 90 | @PutMapping("/{id}") 91 | public ResponseEntity> updateDepartment( 92 | @Valid @RequestBody DepartmentRequestDTO departmentRequestDTO, 93 | @PathVariable("id") Long id) { 94 | 95 | DepartmentDTO departmentDTO = departmentMapper.departmentRequestDTOToDepartmentDTO(departmentRequestDTO); 96 | 97 | DepartmentDTO result = departmentService.updateDepartment(departmentDTO, id); 98 | 99 | // Builder Design pattern 100 | APIResponse responseDTO = APIResponse 101 | .builder() 102 | .status(Status.SUCCESS.getValue()) 103 | .results(result) 104 | .build(); 105 | 106 | 107 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 108 | 109 | } 110 | 111 | 112 | @Operation(summary = "Delete a department by ID") 113 | @DeleteMapping("/{id}") 114 | public ResponseEntity> deleteDepartment(@PathVariable("id") Long id) { 115 | 116 | departmentService.deleteDepartment(id); 117 | 118 | String result = "Department deleted successfully"; 119 | 120 | // Builder Design pattern 121 | APIResponse responseDTO = APIResponse 122 | .builder() 123 | .status(Status.SUCCESS.getValue()) 124 | .results(result) 125 | .build(); 126 | 127 | 128 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/repository/DepartmentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.repository; 2 | 3 | import com.ainigma100.departmentapi.dto.DepartmentSearchCriteriaDTO; 4 | import com.ainigma100.departmentapi.entity.Department; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Tag; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.data.domain.Page; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.test.context.ActiveProfiles; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | /* 17 | * @DataJpaTest will automatically configure in-memory database for testing 18 | * and, it will not load annotated beans into the Application Context. 19 | * It will only load the repository class. Tests annotated with @DataJpaTest 20 | * are by default transactional and roll back at the end of each test. 21 | */ 22 | @DataJpaTest 23 | @ActiveProfiles("test") 24 | @Tag("unit") 25 | class DepartmentRepositoryTest { 26 | 27 | @Autowired 28 | private DepartmentRepository departmentRepository; 29 | 30 | private Department department1; 31 | private Department department2; 32 | 33 | /** 34 | * This method will be executed before each and every test inside this class 35 | */ 36 | @BeforeEach 37 | void setUp() { 38 | 39 | department1 = new Department(); 40 | department1.setDepartmentCode("ABC"); 41 | department1.setDepartmentName("Department 1"); 42 | department1.setDepartmentDescription("Description 1"); 43 | 44 | department2 = new Department(); 45 | department2.setDepartmentCode("DEF"); 46 | department2.setDepartmentName("Department 2"); 47 | department2.setDepartmentDescription("Description 2"); 48 | 49 | } 50 | 51 | @Test 52 | void givenDepartmentCode_whenFindByDepartmentCode_thenReturnDepartment() { 53 | 54 | // given - precondition or setup 55 | departmentRepository.save(department1); 56 | 57 | 58 | // when - action or behaviour that we are going to test 59 | Department departmentFromDb = departmentRepository.findByDepartmentCode("ABC"); 60 | 61 | // then - verify the output 62 | assertThat(departmentFromDb).isNotNull(); 63 | assertThat(departmentFromDb.getDepartmentCode()).isEqualTo("ABC"); 64 | assertThat(departmentFromDb.getDepartmentName()).isEqualTo("Department 1"); 65 | assertThat(departmentFromDb.getDepartmentDescription()).isEqualTo("Description 1"); 66 | } 67 | 68 | @Test 69 | void givenDepartmentSearchCriteriaDTO_whenGetAllDepartmentsUsingPagination_thenReturnDepartmentPage() { 70 | 71 | // given - precondition or setup 72 | departmentRepository.save(department1); 73 | departmentRepository.save(department2); 74 | 75 | 76 | DepartmentSearchCriteriaDTO searchCriteria = new DepartmentSearchCriteriaDTO(); 77 | searchCriteria.setDepartmentCode("ABC"); 78 | searchCriteria.setDepartmentName("Depa"); 79 | 80 | PageRequest pageRequest = PageRequest.of(0, 10); 81 | 82 | // when - action or behaviour that we are going to test 83 | Page departmentPage = departmentRepository.getAllDepartmentsUsingPagination(searchCriteria, pageRequest); 84 | 85 | // then - verify the output 86 | assertThat(departmentPage).isNotNull(); 87 | assertThat(departmentPage.getContent()).hasSize(1); 88 | assertThat(departmentPage.getContent().get(0).getDepartmentCode()).isEqualTo("ABC"); 89 | assertThat(departmentPage.getContent().get(0).getDepartmentName()).isEqualTo("Department 1"); 90 | assertThat(departmentPage.getContent().get(0).getDepartmentDescription()).isEqualTo("Description 1"); 91 | } 92 | 93 | @Test 94 | void givenDepartmentSearchCriteriaDTOWithCaseInsensitiveValues_whenGetAllDepartmentsUsingPagination_thenReturnDepartmentPage() { 95 | 96 | // given - precondition or setup 97 | departmentRepository.save(department1); 98 | departmentRepository.save(department2); 99 | 100 | 101 | DepartmentSearchCriteriaDTO searchCriteria = new DepartmentSearchCriteriaDTO(); 102 | searchCriteria.setDepartmentCode("AbC"); 103 | searchCriteria.setDepartmentName("DePArt"); 104 | 105 | PageRequest pageRequest = PageRequest.of(0, 10); 106 | 107 | // when - action or behaviour that we are going to test 108 | Page departmentPage = departmentRepository.getAllDepartmentsUsingPagination(searchCriteria, pageRequest); 109 | 110 | // then - verify the output 111 | assertThat(departmentPage).isNotNull(); 112 | assertThat(departmentPage.getContent()).hasSize(1); 113 | assertThat(departmentPage.getContent().get(0).getDepartmentCode()).isEqualTo("ABC"); 114 | assertThat(departmentPage.getContent().get(0).getDepartmentName()).isEqualTo("Department 1"); 115 | assertThat(departmentPage.getContent().get(0).getDepartmentDescription()).isEqualTo("Description 1"); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.config; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import io.swagger.v3.oas.models.OpenAPI; 7 | import io.swagger.v3.oas.models.info.Info; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.web.client.RestClient; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.net.InetAddress; 19 | import java.net.UnknownHostException; 20 | import java.util.Optional; 21 | 22 | /** 23 | * Configuration class for generating OpenAPI documentation. 24 | * 25 | *

This class configures and generates the OpenAPI documentation for the application 26 | * using the springdoc-openapi library. It automatically generates and formats the OpenAPI 27 | * JSON file based on the application's REST endpoints. The generated JSON file is then 28 | * stored in the root directory of the project for easy access and reference.

29 | */ 30 | @Slf4j 31 | @Configuration 32 | public class OpenApiConfig { 33 | 34 | 35 | private final Environment environment; 36 | 37 | public OpenApiConfig(Environment environment) { 38 | this.environment = environment; 39 | } 40 | 41 | @Value("${server.port:8080}") 42 | private int serverPort; 43 | 44 | @Value("${openapi.output.file}") 45 | private String outputFileName; 46 | 47 | private static final String SERVER_SSL_KEY_STORE = "server.ssl.key-store"; 48 | private static final String SERVER_SERVLET_CONTEXT_PATH = "server.servlet.context-path"; 49 | 50 | @Bean 51 | public OpenAPI customOpenAPI() { 52 | 53 | String documentationVersion = environment.getProperty("springdoc.version", "1.0"); 54 | String appTitle = environment.getProperty("springdoc.title", "API Documentation"); 55 | 56 | 57 | String[] activeProfiles = environment.getActiveProfiles(); 58 | String profileInfo = activeProfiles.length > 0 59 | ? String.join(", ", activeProfiles).toUpperCase() 60 | : "DEFAULT"; 61 | 62 | String description = String.format("Active profile: %s", profileInfo); 63 | 64 | return new OpenAPI() 65 | .info(new Info() 66 | .title(appTitle) 67 | .version(documentationVersion) 68 | .description(description)); 69 | } 70 | 71 | 72 | 73 | @Bean 74 | public CommandLineRunner generateOpenApiJson() { 75 | return args -> { 76 | String protocol = Optional.ofNullable(environment.getProperty(SERVER_SSL_KEY_STORE)).map(key -> "https").orElse("http"); 77 | String host = getServerIP(); 78 | String contextPath = Optional.ofNullable(environment.getProperty(SERVER_SERVLET_CONTEXT_PATH)).orElse(""); 79 | 80 | // Define the API docs URL 81 | String apiDocsUrl = String.format("%s://%s:%d%s/v3/api-docs", protocol, host, serverPort, contextPath); 82 | 83 | log.info("Attempting to fetch OpenAPI docs from URL: {}", apiDocsUrl); 84 | 85 | try { 86 | // Create RestClient instance 87 | RestClient restClient = RestClient.create(); 88 | 89 | // Fetch the OpenAPI JSON 90 | String response = restClient.get() 91 | .uri(apiDocsUrl) 92 | .retrieve() 93 | .body(String.class); 94 | 95 | // Format and save the JSON to a file 96 | formatAndSaveToFile(response, outputFileName); 97 | 98 | log.info("OpenAPI documentation generated successfully at {}", outputFileName); 99 | 100 | } catch (Exception e) { 101 | log.error("Failed to generate OpenAPI documentation from URL: {}", apiDocsUrl, e); 102 | } 103 | }; 104 | } 105 | 106 | private String getServerIP() { 107 | try { 108 | return InetAddress.getLocalHost().getHostAddress(); 109 | } catch (UnknownHostException e) { 110 | log.error("Error resolving host address", e); 111 | return "unknown"; 112 | } 113 | } 114 | 115 | private void formatAndSaveToFile(String content, String fileName) { 116 | try { 117 | ObjectMapper objectMapper = new ObjectMapper(); 118 | 119 | // Enable pretty-print 120 | objectMapper.enable(SerializationFeature.INDENT_OUTPUT); 121 | 122 | // Read the JSON content as a JsonNode 123 | JsonNode jsonNode = objectMapper.readTree(content); 124 | 125 | // Write the formatted JSON to a file 126 | objectMapper.writeValue(new File(fileName), jsonNode); 127 | 128 | } catch (IOException e) { 129 | log.error("Error while saving JSON to file", e); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/service/impl/EmailServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service.impl; 2 | 3 | import com.ainigma100.departmentapi.dto.FileDTO; 4 | import com.ainigma100.departmentapi.enums.ReportLanguage; 5 | import com.ainigma100.departmentapi.service.ReportService; 6 | import com.ainigma100.departmentapi.utils.TestHelper; 7 | import com.ainigma100.departmentapi.utils.email.EmailRequest; 8 | import com.ainigma100.departmentapi.utils.email.EmailSender; 9 | import net.sf.jasperreports.engine.JRException; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Tag; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.ArgumentCaptor; 15 | import org.mockito.Captor; 16 | import org.mockito.InjectMocks; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | import java.io.IOException; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.mockito.BDDMockito.willDoNothing; 26 | import static org.mockito.Mockito.times; 27 | import static org.mockito.Mockito.verify; 28 | 29 | /* 30 | * @ExtendWith(MockitoExtension.class) informs Mockito that we are using 31 | * mockito annotations to mock the dependencies 32 | */ 33 | @ExtendWith(MockitoExtension.class) 34 | @Tag("unit") 35 | class EmailServiceImplTest { 36 | 37 | @Mock 38 | private EmailSender emailSender; 39 | 40 | @Mock 41 | private ReportService reportService; 42 | 43 | // @InjectMocks creates the mock object of the class and injects the mocks 44 | // that are marked with the annotations @Mock into it. 45 | @InjectMocks 46 | private EmailServiceImpl emailService; 47 | 48 | // @Captor used to capture and store the arguments passed to a mocked method for further assertions or verifications. 49 | @Captor 50 | private ArgumentCaptor emailRequestCaptor; 51 | 52 | private FileDTO fileDTO; 53 | 54 | 55 | @BeforeEach 56 | void setUp() throws IOException { 57 | 58 | String filePath = "jsonfile/mockedFileDTO.json"; 59 | 60 | String fileName = TestHelper.extractJsonPropertyFromFile(filePath, "fileName"); 61 | byte[] fileContent = TestHelper.extractJsonPropertyFromFile(filePath, "fileContent").getBytes(); 62 | 63 | fileDTO = new FileDTO(fileName, fileContent); 64 | 65 | } 66 | 67 | 68 | 69 | @Test 70 | void givenNoInput_whenSendEmailWithoutAttachment_thenReturnTrue() { 71 | 72 | // given - precondition or setup 73 | willDoNothing().given(emailSender).sendEmail(any(EmailRequest.class)); 74 | 75 | // when - action or behaviour that we are going to test 76 | Boolean result = emailService.sendEmailWithoutAttachment(); 77 | 78 | // then - verify the output and interactions 79 | verify(emailSender, times(1)).sendEmail(emailRequestCaptor.capture()); 80 | 81 | 82 | EmailRequest capturedEmailRequest = emailRequestCaptor.getValue(); 83 | assertThat(capturedEmailRequest.getFrom()).isEqualTo("lyffy@pirateking.com"); 84 | assertThat(capturedEmailRequest.getSubject()).isEqualTo("Test Email"); 85 | assertThat(capturedEmailRequest.getEmailBody()).isEqualTo("email-template.html"); 86 | assertThat(capturedEmailRequest.getToRecipients()).hasSize(1).contains("jwick@gmail.com"); 87 | assertThat(capturedEmailRequest.getCcRecipients()).hasSize(1).contains("mpolo@gmail.com"); 88 | assertThat(capturedEmailRequest.getDynamicVariables()).hasSize(2) 89 | .containsEntry("recipientName", "John Wick") 90 | .containsEntry("githubRepoUrl", "https://github.com/pmoustopoulos/department-api"); 91 | assertThat(capturedEmailRequest.getImagePaths()).hasSize(1).containsEntry("logo", "reportLogo/luffy.png"); 92 | assertThat(capturedEmailRequest.getAttachments()).isNull(); 93 | 94 | assertThat(result).isTrue(); 95 | } 96 | 97 | @Test 98 | void givenNoInput_whenSendEmailWithAttachment_thenReturnTrue() throws JRException { 99 | 100 | // given - precondition or setup 101 | given(reportService.generateDepartmentsExcelReport()).willReturn(fileDTO); 102 | given(reportService.generatePdfFullReport(any(ReportLanguage.class))).willReturn(fileDTO); 103 | willDoNothing().given(emailSender).sendEmail(any(EmailRequest.class)); 104 | 105 | // when - action or behaviour that we are going to test 106 | Boolean result = emailService.sendEmailWithAttachment(); 107 | 108 | // then - verify the output and interactions 109 | verify(emailSender, times(1)).sendEmail(emailRequestCaptor.capture()); 110 | 111 | EmailRequest capturedEmailRequest = emailRequestCaptor.getValue(); 112 | assertThat(capturedEmailRequest.getFrom()).isEqualTo("lyffy@pirateking.com"); 113 | assertThat(capturedEmailRequest.getSubject()).isEqualTo("Test Email"); 114 | assertThat(capturedEmailRequest.getEmailBody()).isEqualTo("email-template.html"); 115 | assertThat(capturedEmailRequest.getToRecipients()).hasSize(2).contains("jwick@gmail.com", "maria@gmail.com"); 116 | assertThat(capturedEmailRequest.getCcRecipients()).hasSize(2).contains("mpolo@gmail.com", "nick@gmail.com"); 117 | assertThat(capturedEmailRequest.getDynamicVariables()).hasSize(2) 118 | .containsEntry("recipientName", "John Wick") 119 | .containsEntry("githubRepoUrl", "https://github.com/pmoustopoulos/department-api"); 120 | assertThat(capturedEmailRequest.getImagePaths()).hasSize(1).containsEntry("logo", "reportLogo/luffy.png"); 121 | assertThat(capturedEmailRequest.getAttachments()).isNotNull(); 122 | 123 | assertThat(result).isTrue(); 124 | } 125 | 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/controller/ReportControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.dto.FileDTO; 4 | import com.ainigma100.departmentapi.enums.ReportLanguage; 5 | import com.ainigma100.departmentapi.filter.RateLimitingFilter; 6 | import com.ainigma100.departmentapi.service.ReportService; 7 | import com.ainigma100.departmentapi.utils.TestHelper; 8 | import org.junit.jupiter.api.*; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.ResultActions; 15 | 16 | import java.io.IOException; 17 | 18 | import static org.mockito.BDDMockito.given; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 22 | 23 | /* 24 | * @WebMvcTest annotation will load all the components required 25 | * to test the Controller layer. It will not load the service or repository layer components 26 | */ 27 | @WebMvcTest(ReportController.class) 28 | @Tag("unit") 29 | class ReportControllerTest { 30 | 31 | @Autowired 32 | private MockMvc mockMvc; 33 | 34 | @MockitoBean 35 | private ReportService reportService; 36 | 37 | @Autowired 38 | private RateLimitingFilter rateLimitingFilter; 39 | 40 | 41 | private FileDTO report; 42 | 43 | 44 | @BeforeEach 45 | void setUp() throws IOException { 46 | 47 | String filePath = "jsonfile/mockedFileDTO.json"; 48 | 49 | String fileName = TestHelper.extractJsonPropertyFromFile(filePath, "fileName"); 50 | byte[] fileContent = TestHelper.extractJsonPropertyFromFile(filePath, "fileContent").getBytes(); 51 | 52 | report = new FileDTO(fileName, fileContent); 53 | 54 | } 55 | 56 | @AfterEach 57 | void resetRateLimitBuckets() { 58 | rateLimitingFilter.clearBuckets(); 59 | } 60 | 61 | 62 | @Test 63 | @DisplayName("Generate an Excel report containing all the departments") 64 | void givenNoInput_whenGenerateDepartmentsExcelReport_thenReturnInputStreamResource() throws Exception { 65 | 66 | // given - precondition or setup 67 | String filePath = "jsonfile/mockedFileDTO.json"; 68 | 69 | 70 | String fileName = TestHelper.extractJsonPropertyFromFile(filePath, "fileName"); 71 | byte[] fileContent = TestHelper.extractJsonPropertyFromFile(filePath, "fileContent").getBytes(); 72 | 73 | FileDTO report = new FileDTO(fileName, fileContent); 74 | 75 | given(reportService.generateDepartmentsExcelReport()).willReturn(report); 76 | 77 | // when - action or behaviour that we are going to test 78 | ResultActions response = mockMvc.perform(get("/api/v1/reports/excel/departments")); 79 | 80 | // then - verify the output 81 | response.andDo(print()) 82 | // verify the status code that is returned 83 | .andExpect(status().isOk()); 84 | 85 | } 86 | 87 | @Test 88 | @DisplayName("Generate an Excel report containing all the employees") 89 | void givenNoInput_whenGenerateEmployeesExcelReport_thenReturnInputStreamResource() throws Exception { 90 | 91 | // given - precondition or setup 92 | given(reportService.generateEmployeesExcelReport()).willReturn(report); 93 | 94 | // when - action or behaviour that we are going to test 95 | ResultActions response = mockMvc.perform(get("/api/v1/reports/excel/employees")); 96 | 97 | // then - verify the output 98 | response.andDo(print()) 99 | // verify the status code that is returned 100 | .andExpect(status().isOk()); 101 | 102 | } 103 | 104 | @Test 105 | @DisplayName("Generate a PDF report containing all the departments along with all the employees in the specified language") 106 | void givenReportLanguage_whenGeneratePdfFullReport_thenReturnInputStreamResource() throws Exception { 107 | 108 | // given - precondition or setup 109 | ReportLanguage reportLanguage = ReportLanguage.EN; 110 | given(reportService.generatePdfFullReport(reportLanguage)).willReturn(report); 111 | 112 | // when - action or behaviour that we are going to test 113 | ResultActions response = mockMvc.perform(get("/api/v1/reports/pdf/full-report") 114 | .contentType(MediaType.APPLICATION_JSON) 115 | .param("language", String.valueOf(reportLanguage))); 116 | 117 | // then - verify the output 118 | response.andDo(print()) 119 | // verify the status code that is returned 120 | .andExpect(status().isOk()); 121 | 122 | } 123 | 124 | @Test 125 | @DisplayName("Generate a combined PDF report from two separate reports in the specified language") 126 | void givenReportLanguage_whenGenerateCombinedPdfReport_thenReturnInputStreamResource() throws Exception { 127 | 128 | // given - precondition or setup 129 | ReportLanguage reportLanguage = ReportLanguage.EN; 130 | given(reportService.generateCombinedPdfReport(reportLanguage)).willReturn(report); 131 | 132 | // when - action or behaviour that we are going to test 133 | ResultActions response = mockMvc.perform(get("/api/v1/reports/pdf/combined-report") 134 | .contentType(MediaType.APPLICATION_JSON) 135 | .param("language", String.valueOf(reportLanguage))); 136 | 137 | // then - verify the output 138 | response.andDo(print()) 139 | // verify the status code that is returned 140 | .andExpect(status().isOk()); 141 | 142 | } 143 | 144 | 145 | 146 | @Test 147 | @DisplayName("Generate a zip file which contains two excel reports") 148 | void givenNoInput_whenGenerateAndZipReports_thenReturnInputStreamResource() throws Exception { 149 | 150 | // given - precondition or setup 151 | given(reportService.generateAndZipReports()).willReturn(report); 152 | 153 | // when - action or behaviour that we are going to test 154 | ResultActions response = mockMvc.perform(get("/api/v1/reports/zip")); 155 | 156 | // then - verify the output 157 | response.andDo(print()) 158 | // verify the status code that is returned 159 | .andExpect(status().isOk()); 160 | 161 | } 162 | 163 | @Test 164 | @DisplayName("Generate a multi-sheet Excel report containing departments and employees") 165 | void givenNoInput_whenGenerateMultiSheetExcelReport_thenReturnInputStreamResource() throws Exception { 166 | 167 | // given - precondition or setup 168 | given(reportService.generateMultiSheetExcelReport()).willReturn(report); 169 | 170 | // when - action or behaviour that we are going to test 171 | ResultActions response = mockMvc.perform(get("/api/v1/reports/multi-sheet-excel")); 172 | 173 | // then - verify the output 174 | response.andDo(print()) 175 | // verify the status code that is returned 176 | .andExpect(status().isOk()); 177 | 178 | } 179 | 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/controller/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.dto.*; 4 | import com.ainigma100.departmentapi.enums.Status; 5 | import com.ainigma100.departmentapi.mapper.EmployeeMapper; 6 | import com.ainigma100.departmentapi.service.EmployeeService; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import jakarta.validation.Valid; 9 | import lombok.AllArgsConstructor; 10 | import org.springframework.data.domain.Page; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.util.List; 16 | 17 | @AllArgsConstructor 18 | @RequestMapping("/api/v1") 19 | @RestController 20 | public class EmployeeController { 21 | 22 | private final EmployeeService employeeService; 23 | private final EmployeeMapper employeeMapper; 24 | 25 | 26 | 27 | 28 | @Operation(summary = "Add a new employee to the specific department") 29 | @PostMapping("/departments/{departmentId}/employees") 30 | public ResponseEntity> createEmployee( 31 | @PathVariable("departmentId") Long departmentId, 32 | @Valid @RequestBody EmployeeRequestDTO employeeRequestDTO) { 33 | 34 | EmployeeDTO employeeDTO = employeeMapper.employeeRequestDTOToEmployeeDTO(employeeRequestDTO); 35 | 36 | EmployeeDTO result = employeeService.createEmployee(departmentId, employeeDTO); 37 | 38 | // Builder Design pattern 39 | APIResponse responseDTO = APIResponse 40 | .builder() 41 | .status(Status.SUCCESS.getValue()) 42 | .results(result) 43 | .build(); 44 | 45 | 46 | return new ResponseEntity<>(responseDTO, HttpStatus.CREATED); 47 | } 48 | 49 | 50 | @Operation(summary = "Search all employees from all departments using pagination", 51 | description = "Returns a list of employees belonging to any department") 52 | @PostMapping("/employees/search") 53 | public ResponseEntity>> getAllEmployeesUsingPagination( 54 | @Valid @RequestBody EmployeeSearchCriteriaDTO employeeSearchCriteriaDTO) { 55 | 56 | Page result = employeeService.getAllEmployeesUsingPagination(employeeSearchCriteriaDTO); 57 | 58 | // Builder Design pattern 59 | APIResponse> responseDTO = APIResponse 60 | .>builder() 61 | .status(Status.SUCCESS.getValue()) 62 | .results(result) 63 | .build(); 64 | 65 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 66 | } 67 | 68 | 69 | @Operation(summary = "Find all employees belonging to the specific department", 70 | description = "Returns a list of employees belonging to the specific department") 71 | @GetMapping("/departments/{departmentId}/employees") 72 | public ResponseEntity>> getEmployeesByDepartmentId( 73 | @PathVariable("departmentId") Long departmentId) { 74 | 75 | List result = employeeService.getEmployeesByDepartmentId(departmentId); 76 | 77 | // Builder Design pattern 78 | APIResponse> responseDTO = APIResponse 79 | .>builder() 80 | .status(Status.SUCCESS.getValue()) 81 | .results(result) 82 | .build(); 83 | 84 | 85 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 86 | } 87 | 88 | 89 | @Operation(summary = "Find employee by ID belonging to the specific department", 90 | description = "Returns a single employee belonging to the specific department") 91 | @GetMapping("/departments/{departmentId}/employees/{employeeId}") 92 | public ResponseEntity> getEmployeeById( 93 | @PathVariable("departmentId") Long departmentId, 94 | @PathVariable("employeeId") String employeeId) { 95 | 96 | EmployeeDTO result = employeeService.getEmployeeById(departmentId, employeeId); 97 | 98 | // Builder Design pattern 99 | APIResponse responseDTO = APIResponse 100 | .builder() 101 | .status(Status.SUCCESS.getValue()) 102 | .results(result) 103 | .build(); 104 | 105 | 106 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 107 | } 108 | 109 | 110 | @Operation(summary = "Update an existing employee belonging to the specific department") 111 | @PutMapping("/departments/{departmentId}/employees/{employeeId}") 112 | public ResponseEntity> updateEmployeeById( 113 | @PathVariable("departmentId") Long departmentId, 114 | @PathVariable("employeeId") String employeeId, 115 | @Valid @RequestBody EmployeeRequestDTO employeeRequestDTO) { 116 | 117 | EmployeeDTO employeeDTO = employeeMapper.employeeRequestDTOToEmployeeDTO(employeeRequestDTO); 118 | 119 | EmployeeDTO result = employeeService.updateEmployeeById(departmentId, employeeId, employeeDTO); 120 | 121 | // Builder Design pattern 122 | APIResponse responseDTO = APIResponse 123 | .builder() 124 | .status(Status.SUCCESS.getValue()) 125 | .results(result) 126 | .build(); 127 | 128 | 129 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 130 | } 131 | 132 | 133 | @Operation(summary = "Delete an employee belonging to the specific department") 134 | @DeleteMapping("/departments/{departmentId}/employees/{employeeId}") 135 | public ResponseEntity> deleteEmployee( 136 | @PathVariable("departmentId") Long departmentId, 137 | @PathVariable("employeeId") String employeeId) { 138 | 139 | employeeService.deleteEmployee(departmentId, employeeId); 140 | 141 | String result = "Employee deleted successfully"; 142 | 143 | // Builder Design pattern 144 | APIResponse responseDTO = APIResponse 145 | .builder() 146 | .status(Status.SUCCESS.getValue()) 147 | .results(result) 148 | .build(); 149 | 150 | 151 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 152 | 153 | } 154 | 155 | @Operation(summary = "Get an employee along with the department using the employee email. " + 156 | "Note: This endpoint was created just to demonstrate how to get a child and a parent object using the child's property") 157 | @GetMapping("/employees") 158 | public ResponseEntity> getEmployeeAndDepartmentByEmployeeEmail( 159 | @RequestParam("email") String email) { 160 | 161 | EmployeeAndDepartmentDTO result = employeeService.getEmployeeAndDepartmentByEmployeeEmail(email); 162 | 163 | // Builder Design pattern 164 | APIResponse responseDTO = APIResponse 165 | .builder() 166 | .status(Status.SUCCESS.getValue()) 167 | .results(result) 168 | .build(); 169 | 170 | return new ResponseEntity<>(responseDTO, HttpStatus.OK); 171 | } 172 | 173 | 174 | } 175 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/controller/ReportController.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.controller; 2 | 3 | import com.ainigma100.departmentapi.dto.FileDTO; 4 | import com.ainigma100.departmentapi.enums.ReportLanguage; 5 | import com.ainigma100.departmentapi.service.ReportService; 6 | import io.swagger.v3.oas.annotations.Operation; 7 | import lombok.AllArgsConstructor; 8 | import net.sf.jasperreports.engine.JRException; 9 | import org.springframework.core.io.InputStreamResource; 10 | import org.springframework.http.ContentDisposition; 11 | import org.springframework.http.HttpHeaders; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.nio.charset.StandardCharsets; 23 | 24 | @AllArgsConstructor 25 | @RequestMapping("/api/v1/reports") 26 | @RestController 27 | public class ReportController { 28 | 29 | 30 | private final ReportService reportService; 31 | 32 | 33 | 34 | @Operation(summary = "Generate an Excel report containing all the departments") 35 | @GetMapping("/excel/departments") 36 | public ResponseEntity generateDepartmentsExcelReport() throws JRException { 37 | 38 | FileDTO report = reportService.generateDepartmentsExcelReport(); 39 | 40 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 41 | 42 | HttpHeaders httpHeaders = new HttpHeaders(); 43 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 44 | .filename(report.getFileName(), StandardCharsets.UTF_8) 45 | .build()); 46 | 47 | return ResponseEntity 48 | .ok() 49 | .headers(httpHeaders) 50 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 51 | .contentLength(report.getFileContent().length) 52 | .body(new InputStreamResource(targetStream)); 53 | 54 | } 55 | 56 | 57 | @Operation(summary = "Generate an Excel report containing all the employees") 58 | @GetMapping("/excel/employees") 59 | public ResponseEntity generateEmployeesExcelReport() throws JRException { 60 | 61 | FileDTO report = reportService.generateEmployeesExcelReport(); 62 | 63 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 64 | 65 | HttpHeaders httpHeaders = new HttpHeaders(); 66 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 67 | .filename(report.getFileName(), StandardCharsets.UTF_8) 68 | .build()); 69 | 70 | return ResponseEntity 71 | .ok() 72 | .headers(httpHeaders) 73 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 74 | .contentLength(report.getFileContent().length) 75 | .body(new InputStreamResource(targetStream)); 76 | 77 | } 78 | 79 | 80 | @Operation(summary = "Generate a PDF report containing all the departments along with all the employees in the specified language") 81 | @GetMapping("/pdf/full-report") 82 | public ResponseEntity generatePdfFullReport( 83 | @RequestParam ReportLanguage language) throws JRException { 84 | 85 | FileDTO report = reportService.generatePdfFullReport(language); 86 | 87 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 88 | 89 | HttpHeaders httpHeaders = new HttpHeaders(); 90 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 91 | .filename(report.getFileName(), StandardCharsets.UTF_8) 92 | .build()); 93 | 94 | return ResponseEntity 95 | .ok() 96 | .headers(httpHeaders) 97 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 98 | .contentLength(report.getFileContent().length) 99 | .body(new InputStreamResource(targetStream)); 100 | 101 | } 102 | 103 | @Operation(summary = "Generate a combined PDF report from two separate reports in the specified language") 104 | @GetMapping("/pdf/combined-report") 105 | public ResponseEntity generateCombinedPdfReport( 106 | @RequestParam ReportLanguage language) throws JRException { 107 | 108 | FileDTO report = reportService.generateCombinedPdfReport(language); 109 | 110 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 111 | 112 | HttpHeaders httpHeaders = new HttpHeaders(); 113 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 114 | .filename(report.getFileName(), StandardCharsets.UTF_8) 115 | .build()); 116 | 117 | return ResponseEntity 118 | .ok() 119 | .headers(httpHeaders) 120 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 121 | .contentLength(report.getFileContent().length) 122 | .body(new InputStreamResource(targetStream)); 123 | } 124 | 125 | 126 | @Operation(summary = "Generate a zip file which contains two excel reports") 127 | @GetMapping("/zip") 128 | public ResponseEntity generateAndZipReports() throws JRException, IOException { 129 | 130 | FileDTO report = reportService.generateAndZipReports(); 131 | 132 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 133 | 134 | HttpHeaders httpHeaders = new HttpHeaders(); 135 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 136 | .filename(report.getFileName(), StandardCharsets.UTF_8) 137 | .build()); 138 | 139 | return ResponseEntity 140 | .ok() 141 | .headers(httpHeaders) 142 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 143 | .contentLength(report.getFileContent().length) 144 | .body(new InputStreamResource(targetStream)); 145 | 146 | } 147 | 148 | 149 | @Operation(summary = "Generate a multi-sheet Excel report containing departments and employees") 150 | @GetMapping("/multi-sheet-excel") 151 | public ResponseEntity generateMultiSheetExcelReport() throws JRException { 152 | 153 | FileDTO report = reportService.generateMultiSheetExcelReport(); 154 | 155 | InputStream targetStream = new ByteArrayInputStream(report.getFileContent()); 156 | 157 | HttpHeaders httpHeaders = new HttpHeaders(); 158 | httpHeaders.setContentDisposition(ContentDisposition.attachment() 159 | .filename(report.getFileName(), StandardCharsets.UTF_8) 160 | .build()); 161 | 162 | return ResponseEntity 163 | .ok() 164 | .headers(httpHeaders) 165 | .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) 166 | .contentLength(report.getFileContent().length) 167 | .body(new InputStreamResource(targetStream)); 168 | } 169 | 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/repository/EmployeeRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.repository; 2 | 3 | import com.ainigma100.departmentapi.dto.EmployeeSearchCriteriaDTO; 4 | import com.ainigma100.departmentapi.entity.Department; 5 | import com.ainigma100.departmentapi.entity.Employee; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Tag; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.PageRequest; 13 | import org.springframework.test.context.ActiveProfiles; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | /* 21 | * @DataJpaTest will automatically configure in-memory database for testing 22 | * and, it will not load annotated beans into the Application Context. 23 | * It will only load the repository class. Tests annotated with @DataJpaTest 24 | * are by default transactional and roll back at the end of each test. 25 | */ 26 | @DataJpaTest 27 | @ActiveProfiles("test") 28 | @Tag("unit") 29 | class EmployeeRepositoryTest { 30 | 31 | @Autowired 32 | private EmployeeRepository employeeRepository; 33 | 34 | @Autowired 35 | private DepartmentRepository departmentRepository; 36 | 37 | 38 | private Employee employee1; 39 | private Employee employee2; 40 | private Department department; 41 | 42 | 43 | @BeforeEach 44 | void setUp() { 45 | 46 | department = new Department(); 47 | department.setDepartmentCode("ABC"); 48 | department.setDepartmentName("Department 1"); 49 | department.setDepartmentDescription("Description 1"); 50 | 51 | employee1 = new Employee(); 52 | employee1.setFirstName("John"); 53 | employee1.setLastName("Wick"); 54 | employee1.setEmail("jwick@gmail.com"); 55 | employee1.setSalary(BigDecimal.valueOf(40_000_000)); 56 | employee1.setDepartment(department); 57 | 58 | employee2 = new Employee(); 59 | employee2.setFirstName("Luffy"); 60 | employee2.setLastName("Monkey D."); 61 | employee2.setEmail("mluffy@gmail.com"); 62 | employee2.setSalary(BigDecimal.valueOf(50_000_000)); 63 | employee2.setDepartment(department); 64 | 65 | } 66 | 67 | 68 | 69 | @Test 70 | void givenDepartmentId_whenFindByDepartmentId_thenReturnEmployeeList() { 71 | 72 | // given - precondition or setup 73 | departmentRepository.save(department); 74 | employeeRepository.save(employee1); 75 | employeeRepository.save(employee2); 76 | 77 | // when - action or behaviour that we are going to test 78 | List employeeList = employeeRepository.findByDepartmentId(department.getId()); 79 | 80 | // then - verify the output 81 | assertThat(employeeList).isNotNull(); 82 | assertThat(employeeList).hasSize(2); 83 | assertThat(employeeList.get(0).getFirstName()).isEqualTo("John"); 84 | assertThat(employeeList.get(0).getLastName()).isEqualTo("Wick"); 85 | assertThat(employeeList.get(1).getFirstName()).isEqualTo("Luffy"); 86 | assertThat(employeeList.get(1).getSalary()).isEqualByComparingTo(BigDecimal.valueOf(50_000_000)); 87 | 88 | } 89 | 90 | @Test 91 | void givenEmployeeSearchCriteriaDTO_whenGetAllEmployeesUsingPagination_thenEmployeePage() { 92 | 93 | // given - precondition or setup 94 | departmentRepository.save(department); 95 | employeeRepository.save(employee1); 96 | 97 | EmployeeSearchCriteriaDTO searchCriteria = new EmployeeSearchCriteriaDTO(); 98 | searchCriteria.setFirstName("John"); 99 | 100 | PageRequest pageRequest = PageRequest.of(0, 10); 101 | 102 | // when - action or behaviour that we are going to test 103 | Page employeePage = employeeRepository.getAllEmployeesUsingPagination(searchCriteria, pageRequest); 104 | 105 | // then - verify the output 106 | assertThat(employeePage).isNotNull(); 107 | assertThat(employeePage.getContent()).hasSize(1); 108 | assertThat(employeePage.getContent().get(0).getFirstName()).isEqualTo("John"); 109 | assertThat(employeePage.getContent().get(0).getEmail()).isEqualTo("jwick@gmail.com"); 110 | assertThat(employeePage.getContent().get(0).getSalary()).isEqualByComparingTo(BigDecimal.valueOf(40_000_000)); 111 | 112 | } 113 | 114 | @Test 115 | void givenEmployeeSearchCriteriaDTOWithCaseInsensitiveValues_whenGetAllEmployeesUsingPagination_thenEmployeePage() { 116 | 117 | // given - precondition or setup 118 | departmentRepository.save(department); 119 | employeeRepository.save(employee1); 120 | 121 | EmployeeSearchCriteriaDTO searchCriteria = new EmployeeSearchCriteriaDTO(); 122 | searchCriteria.setFirstName("joHN"); 123 | 124 | PageRequest pageRequest = PageRequest.of(0, 10); 125 | 126 | // when - action or behaviour that we are going to test 127 | Page employeePage = employeeRepository.getAllEmployeesUsingPagination(searchCriteria, pageRequest); 128 | 129 | // then - verify the output 130 | assertThat(employeePage).isNotNull(); 131 | assertThat(employeePage.getContent()).hasSize(1); 132 | assertThat(employeePage.getContent().get(0).getFirstName()).isEqualTo("John"); 133 | assertThat(employeePage.getContent().get(0).getEmail()).isEqualTo("jwick@gmail.com"); 134 | assertThat(employeePage.getContent().get(0).getSalary()).isEqualByComparingTo(BigDecimal.valueOf(40_000_000)); 135 | 136 | } 137 | 138 | @Test 139 | void givenEmail_whenFindByEmail_thenReturnEmployee() { 140 | 141 | // given - precondition or setup 142 | departmentRepository.save(department); 143 | employeeRepository.save(employee1); 144 | 145 | // when - action or behaviour that we are going to test 146 | Employee employeeFromDb = employeeRepository.findByEmail(employee1.getEmail()); 147 | 148 | // then - verify the output 149 | assertThat(employeeFromDb).isNotNull(); 150 | assertThat(employeeFromDb.getFirstName()).isEqualTo("John"); 151 | assertThat(employeeFromDb.getLastName()).isEqualTo("Wick"); 152 | assertThat(employeeFromDb.getEmail()).isEqualTo("jwick@gmail.com"); 153 | assertThat(employeeFromDb.getSalary()).isEqualByComparingTo(BigDecimal.valueOf(40_000_000)); 154 | assertThat(employeeFromDb.getDepartment()).isNotNull(); 155 | 156 | } 157 | 158 | @Test 159 | void givenEmail_whenGetEmployeeAndDepartmentByEmployeeEmail_thenReturnEmployee() { 160 | 161 | // given - precondition or setup 162 | departmentRepository.save(department); 163 | employeeRepository.save(employee1); 164 | 165 | // when - action or behaviour that we are going to test 166 | Employee employeeFromDb = employeeRepository.getEmployeeAndDepartmentByEmployeeEmail(employee1.getEmail()); 167 | 168 | // then - verify the output 169 | assertThat(employeeFromDb).isNotNull(); 170 | assertThat(employeeFromDb.getFirstName()).isEqualTo("John"); 171 | assertThat(employeeFromDb.getLastName()).isEqualTo("Wick"); 172 | assertThat(employeeFromDb.getEmail()).isEqualTo("jwick@gmail.com"); 173 | assertThat(employeeFromDb.getSalary()).isEqualByComparingTo(BigDecimal.valueOf(40_000_000)); 174 | assertThat(employeeFromDb.getDepartment()).isNotNull(); 175 | 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/service/impl/EmployeeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.service.impl; 2 | 3 | import com.ainigma100.departmentapi.dto.EmployeeAndDepartmentDTO; 4 | import com.ainigma100.departmentapi.dto.EmployeeDTO; 5 | import com.ainigma100.departmentapi.dto.EmployeeSearchCriteriaDTO; 6 | import com.ainigma100.departmentapi.entity.Department; 7 | import com.ainigma100.departmentapi.entity.Employee; 8 | import com.ainigma100.departmentapi.exception.BusinessLogicException; 9 | import com.ainigma100.departmentapi.mapper.EmployeeMapper; 10 | import com.ainigma100.departmentapi.repository.DepartmentRepository; 11 | import com.ainigma100.departmentapi.repository.EmployeeRepository; 12 | import com.ainigma100.departmentapi.service.EmployeeService; 13 | import com.ainigma100.departmentapi.utils.SortItem; 14 | import com.ainigma100.departmentapi.utils.Utils; 15 | import jakarta.persistence.EntityExistsException; 16 | import jakarta.persistence.EntityNotFoundException; 17 | import lombok.AllArgsConstructor; 18 | import org.springframework.data.domain.Page; 19 | import org.springframework.data.domain.PageImpl; 20 | import org.springframework.data.domain.Pageable; 21 | import org.springframework.stereotype.Service; 22 | 23 | import java.util.List; 24 | 25 | 26 | @AllArgsConstructor 27 | @Service 28 | public class EmployeeServiceImpl implements EmployeeService { 29 | 30 | private final EmployeeRepository employeeRepository; 31 | private final DepartmentRepository departmentRepository; 32 | private final EmployeeMapper employeeMapper; 33 | 34 | private static final String EMPLOYEE_NOT_BELONG_TO_DEPARTMENT = "Employee does not belong to Department"; 35 | 36 | 37 | @Override 38 | public EmployeeDTO createEmployee(Long departmentId, EmployeeDTO employeeDTO) { 39 | 40 | Employee employeeRecordFromDB = employeeRepository.findByEmail(employeeDTO.getEmail()); 41 | 42 | if (employeeRecordFromDB != null) { 43 | throw new EntityExistsException("Employee with email '" + employeeDTO.getEmail() + "' already exists"); 44 | } 45 | 46 | Employee employee = employeeMapper.employeeDtoToEmployee(employeeDTO); 47 | 48 | Department departmentRecordFromDB = departmentRepository.findById(departmentId) 49 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + departmentId + "' not found")); 50 | 51 | employee.setDepartment(departmentRecordFromDB); 52 | 53 | Employee savedEmployee = employeeRepository.save(employee); 54 | 55 | return employeeMapper.employeeToEmployeeDto(savedEmployee); 56 | } 57 | 58 | @Override 59 | public Page getAllEmployeesUsingPagination(EmployeeSearchCriteriaDTO employeeSearchCriteriaDTO) { 60 | 61 | Integer page = employeeSearchCriteriaDTO.getPage(); 62 | Integer size = employeeSearchCriteriaDTO.getSize(); 63 | List sortList = employeeSearchCriteriaDTO.getSortList(); 64 | 65 | // this pageable will be used for the pagination. 66 | Pageable pageable = Utils.createPageableBasedOnPageAndSizeAndSorting(sortList, page, size); 67 | 68 | Page recordsFromDb = employeeRepository.getAllEmployeesUsingPagination(employeeSearchCriteriaDTO, pageable); 69 | 70 | List result = employeeMapper.employeeToEmployeeDto(recordsFromDb.getContent()); 71 | 72 | return new PageImpl<>(result, pageable, recordsFromDb.getTotalElements()); 73 | } 74 | 75 | 76 | @Override 77 | public List getEmployeesByDepartmentId(Long departmentId) { 78 | 79 | List employeesFromDB = employeeRepository.findByDepartmentId(departmentId); 80 | 81 | return employeeMapper.employeeToEmployeeDto(employeesFromDB); 82 | } 83 | 84 | 85 | @Override 86 | public EmployeeDTO getEmployeeById(Long departmentId, String employeeId) { 87 | 88 | Department departmentRecordFromDB = departmentRepository.findById(departmentId) 89 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + departmentId + "' not found")); 90 | 91 | Employee employeeRecordFromDB = employeeRepository.findById(employeeId) 92 | .orElseThrow(() -> new EntityNotFoundException("Employee with id '" + employeeId + "' not found")); 93 | 94 | 95 | if (!employeeBelongsToDepartment(departmentRecordFromDB, employeeRecordFromDB)) { 96 | throw new BusinessLogicException(EMPLOYEE_NOT_BELONG_TO_DEPARTMENT); 97 | } 98 | 99 | return employeeMapper.employeeToEmployeeDto(employeeRecordFromDB); 100 | } 101 | 102 | @Override 103 | public EmployeeDTO updateEmployeeById(Long departmentId, String employeeId, EmployeeDTO employeeDTO) { 104 | 105 | Department departmentRecordFromDB = departmentRepository.findById(departmentId) 106 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + departmentId + "' not found")); 107 | 108 | Employee employeeRecordFromDB = employeeRepository.findById(employeeId) 109 | .orElseThrow(() -> new EntityNotFoundException("Employee with id '" + employeeId + "' not found")); 110 | 111 | if (!employeeBelongsToDepartment(departmentRecordFromDB, employeeRecordFromDB)) { 112 | throw new BusinessLogicException(EMPLOYEE_NOT_BELONG_TO_DEPARTMENT); 113 | } 114 | 115 | 116 | // just to be safe that the object does not have another id 117 | employeeDTO.setId(employeeId); 118 | 119 | Employee recordToBeSaved = employeeMapper.employeeDtoToEmployee(employeeDTO); 120 | 121 | // assign the post to the current comment 122 | recordToBeSaved.setDepartment(departmentRecordFromDB); 123 | 124 | Employee savedEmployeeRecord = employeeRepository.save(recordToBeSaved); 125 | 126 | return employeeMapper.employeeToEmployeeDto(savedEmployeeRecord); 127 | } 128 | 129 | @Override 130 | public void deleteEmployee(Long departmentId, String employeeId) { 131 | 132 | Department departmentRecordFromDB = departmentRepository.findById(departmentId) 133 | .orElseThrow(() -> new EntityNotFoundException("Department with id '" + departmentId + "' not found")); 134 | 135 | Employee employeeRecordFromDB = employeeRepository.findById(employeeId) 136 | .orElseThrow(() -> new EntityNotFoundException("Employee with id '" + employeeId + "' not found")); 137 | 138 | if (!employeeBelongsToDepartment(departmentRecordFromDB, employeeRecordFromDB)) { 139 | throw new BusinessLogicException(EMPLOYEE_NOT_BELONG_TO_DEPARTMENT); 140 | } 141 | 142 | employeeRepository.delete(employeeRecordFromDB); 143 | 144 | } 145 | 146 | @Override 147 | public EmployeeAndDepartmentDTO getEmployeeAndDepartmentByEmployeeEmail(String email) { 148 | 149 | Employee employeeRecordFromDB = employeeRepository.getEmployeeAndDepartmentByEmployeeEmail(email); 150 | 151 | if (employeeRecordFromDB == null) { 152 | throw new EntityNotFoundException("Employee with email '" + email + "' not found"); 153 | } 154 | 155 | return employeeMapper.employeeToEmployeeAndDepartmentDto(employeeRecordFromDB); 156 | } 157 | 158 | 159 | private boolean employeeBelongsToDepartment(Department departmentRecordFromDB, Employee employeeRecordFromDB) { 160 | 161 | if (departmentRecordFromDB == null || employeeRecordFromDB.getDepartment() == null) { 162 | return false; 163 | } 164 | 165 | Long departmentId = employeeRecordFromDB.getDepartment().getId(); 166 | return departmentId != null && departmentId.equals(departmentRecordFromDB.getId()); 167 | } 168 | 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.exception; 2 | 3 | import com.ainigma100.departmentapi.dto.APIResponse; 4 | import com.ainigma100.departmentapi.dto.ErrorDTO; 5 | import com.ainigma100.departmentapi.enums.Status; 6 | import jakarta.persistence.EntityExistsException; 7 | import jakarta.persistence.EntityNotFoundException; 8 | import jakarta.validation.ConstraintViolation; 9 | import jakarta.validation.ConstraintViolationException; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.core.env.Environment; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.http.converter.HttpMessageNotReadableException; 16 | import org.springframework.validation.FieldError; 17 | import org.springframework.web.HttpRequestMethodNotSupportedException; 18 | import org.springframework.web.bind.MethodArgumentNotValidException; 19 | import org.springframework.web.bind.MissingPathVariableException; 20 | import org.springframework.web.bind.MissingServletRequestParameterException; 21 | import org.springframework.web.bind.annotation.ControllerAdvice; 22 | import org.springframework.web.bind.annotation.ExceptionHandler; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.List; 27 | import java.util.stream.Stream; 28 | 29 | @Slf4j 30 | @ControllerAdvice 31 | @RequiredArgsConstructor 32 | public class GlobalExceptionHandler { 33 | 34 | private final Environment environment; 35 | 36 | /** 37 | * Checks if the application is running in production mode. 38 | * Returns true if the active profile is 'prod' or 'production'. 39 | */ 40 | private boolean isProduction() { 41 | return Stream.of(environment.getActiveProfiles()) 42 | .anyMatch(profile -> profile.equalsIgnoreCase("prod") || profile.equalsIgnoreCase("production")); 43 | } 44 | 45 | 46 | @ExceptionHandler({RuntimeException.class, NullPointerException.class}) 47 | public ResponseEntity handleRuntimeExceptions(RuntimeException exception) { 48 | 49 | APIResponse response = new APIResponse<>(); 50 | response.setStatus(Status.FAILED.getValue()); 51 | 52 | String errorMessage = isProduction() ? "An internal server error occurred" : exception.getMessage(); 53 | response.setErrors(Collections.singletonList(new ErrorDTO("", errorMessage))); 54 | 55 | log.error("RuntimeException or NullPointerException occurred: {}", exception.getMessage(), exception); 56 | 57 | return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); 58 | } 59 | 60 | 61 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 62 | public ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) { 63 | 64 | APIResponse response = new APIResponse<>(); 65 | response.setStatus(Status.FAILED.getValue()); 66 | 67 | String errorMessage = isProduction() ? "Method not supported" : "The requested URL does not support this method"; 68 | response.setErrors(Collections.singletonList(new ErrorDTO("", errorMessage))); 69 | 70 | log.error("HttpRequestMethodNotSupportedException occurred: {}", exception.getMessage(), exception); 71 | 72 | return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED); 73 | } 74 | 75 | 76 | @ExceptionHandler({MethodArgumentNotValidException.class, MissingServletRequestParameterException.class, MissingPathVariableException.class}) 77 | public ResponseEntity handleValidationExceptions(Exception exception) { 78 | 79 | APIResponse response = new APIResponse<>(); 80 | response.setStatus(Status.FAILED.getValue()); 81 | 82 | List errors = new ArrayList<>(); 83 | if (exception instanceof MethodArgumentNotValidException ex) { 84 | 85 | ex.getBindingResult().getAllErrors().forEach(error -> { 86 | String fieldName = ((FieldError) error).getField(); 87 | String errorMessage = isProduction() ? "Invalid input value" : error.getDefaultMessage(); 88 | errors.add(new ErrorDTO(fieldName, errorMessage)); 89 | }); 90 | 91 | } else if (exception instanceof MissingServletRequestParameterException ex) { 92 | 93 | String errorMessage = isProduction() ? "Required parameter is missing" : "Missing parameter: " + ex.getParameterName(); 94 | errors.add(new ErrorDTO("", errorMessage)); 95 | 96 | } else if (exception instanceof MissingPathVariableException ex) { 97 | String errorMessage = isProduction() ? "Missing path variable" : "Missing path variable: " + ex.getVariableName(); 98 | errors.add(new ErrorDTO("", errorMessage)); 99 | } 100 | 101 | log.error("Validation errors: {}", exception.getMessage(), exception); 102 | 103 | response.setErrors(errors); 104 | return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); 105 | } 106 | 107 | 108 | @ExceptionHandler(HttpMessageNotReadableException.class) 109 | public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { 110 | 111 | APIResponse response = new APIResponse<>(); 112 | response.setStatus(Status.FAILED.getValue()); 113 | 114 | String errorMessage = isProduction() ? "Invalid request format" : "Malformed JSON request"; 115 | response.setErrors(Collections.singletonList(new ErrorDTO("", errorMessage))); 116 | 117 | log.error("Malformed JSON request: {}", ex.getMessage(), ex); 118 | 119 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); 120 | } 121 | 122 | 123 | @ExceptionHandler(ConstraintViolationException.class) 124 | public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex) { 125 | 126 | List errors = new ArrayList<>(); 127 | 128 | for (ConstraintViolation violation : ex.getConstraintViolations()) { 129 | errors.add(new ErrorDTO(violation.getPropertyPath().toString(), 130 | isProduction() ? "Invalid input data" : violation.getMessage())); 131 | } 132 | 133 | APIResponse response = new APIResponse<>(); 134 | response.setStatus(Status.FAILED.getValue()); 135 | response.setErrors(errors); 136 | 137 | log.error("Constraint violation errors: {}", ex.getMessage(), ex); 138 | 139 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); 140 | } 141 | 142 | 143 | @ExceptionHandler(EntityNotFoundException.class) 144 | public ResponseEntity handleEntityNotFoundExceptions(EntityNotFoundException exception) { 145 | 146 | APIResponse response = new APIResponse<>(); 147 | response.setStatus(Status.FAILED.getValue()); 148 | 149 | String errorMessage = isProduction() ? "The requested resource was not found" : exception.getMessage(); 150 | response.setErrors(Collections.singletonList(new ErrorDTO("", errorMessage))); 151 | 152 | log.error("EntityNotFoundException occurred: {}", exception.getMessage(), exception); 153 | 154 | return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); 155 | } 156 | 157 | @ExceptionHandler(EntityExistsException.class) 158 | public ResponseEntity> handleEntityExistsException(EntityExistsException exception) { 159 | 160 | APIResponse response = new APIResponse<>(); 161 | response.setStatus(Status.FAILED.getValue()); 162 | 163 | String errorMessage = isProduction() ? "The entity already exists" : exception.getMessage(); 164 | response.setErrors(Collections.singletonList(new ErrorDTO("", errorMessage))); 165 | 166 | log.error("EntityExistsException occurred: {}", exception.getMessage(), exception); 167 | 168 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/ainigma100/departmentapi/utils/jasperreport/SimpleReportExporter.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.utils.jasperreport; 2 | 3 | import com.ainigma100.departmentapi.exception.ReportGenerationException; 4 | import lombok.AllArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import net.sf.jasperreports.engine.JRException; 7 | import net.sf.jasperreports.engine.JRParameter; 8 | import net.sf.jasperreports.engine.JasperExportManager; 9 | import net.sf.jasperreports.engine.JasperPrint; 10 | import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; 11 | import net.sf.jasperreports.engine.export.JRPdfExporter; 12 | import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter; 13 | import net.sf.jasperreports.export.SimpleExporterInput; 14 | import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput; 15 | import net.sf.jasperreports.export.SimplePdfExporterConfiguration; 16 | import org.springframework.stereotype.Component; 17 | 18 | import java.io.ByteArrayInputStream; 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.util.*; 22 | import java.util.zip.ZipEntry; 23 | import java.util.zip.ZipOutputStream; 24 | @Slf4j 25 | @AllArgsConstructor 26 | @Component 27 | public class SimpleReportExporter { 28 | 29 | 30 | private final SimpleReportFiller reportFiller; 31 | 32 | 33 | 34 | /** 35 | * This method is used to generate a JasperPrint by providing the 36 | * list of records, the name of the generated file and the name of the jrxml file 37 | * which will be used as a template to be populated from the records of the list. 38 | * 39 | * @param records to be inserted in the file 40 | * @param jasperParameters this attribute can be used to add additional parameters to JasperReport ( example: local time for the Jasper Report ) 41 | * @param reportFileName with the file extension. Currently supported ( .xlsx and .pdf ). 42 | * @param jrxmlFileName along with the path to it 43 | * @return JasperPrint 44 | */ 45 | public JasperPrint extractResultsToJasperPrint(List records, Map jasperParameters, String reportFileName, String jrxmlFileName) { 46 | 47 | JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(records); 48 | 49 | jasperParameters.put(JRParameter.REPORT_LOCALE, Locale.ITALY); // set Locale to Italy 50 | 51 | try { 52 | JasperPrint jasperPrint = reportFiller.prepareReport(jrxmlFileName, jasperParameters, dataSource); 53 | jasperPrint.setName(reportFileName); 54 | 55 | return jasperPrint; 56 | 57 | } catch (JRException e) { 58 | 59 | log.error("Error generating report occurred in extractResultsToJasperPrint method.", e); 60 | throw new ReportGenerationException("Error generating report occurred in extractResultsToJasperPrint method.", e); 61 | 62 | } finally { 63 | dataSource.cloneDataSource(); 64 | } 65 | } 66 | 67 | 68 | 69 | /** 70 | * This method is used to generate a JasperPrint by providing the 71 | * list of records, the name of the generated file and the name of the jrxml file 72 | * which will be used as a template to be populated from the records of the list. 73 | * 74 | * @param records to be inserted in the file 75 | * @param reportFileName with the file extension. Currently supported ( .xlsx and .pdf ). 76 | * @param jrxmlFileName along with the path to it 77 | * @return JasperPrint 78 | */ 79 | public JasperPrint extractResultsToJasperPrint(List records, String reportFileName, String jrxmlFileName) { 80 | return this.extractResultsToJasperPrint(records, new HashMap<>(), reportFileName, jrxmlFileName); 81 | } 82 | 83 | 84 | /** 85 | * Exports the generated report object received as parameter into Excel format and 86 | * returns the binary content as a byte array. 87 | * 88 | * @param jasperPrint report object to export 89 | * @return byte array representing the resulting PDF content 90 | * @throws JRException 91 | */ 92 | public byte[] exportReportToExcel(JasperPrint jasperPrint) throws JRException { 93 | 94 | if(jasperPrint == null) { 95 | throw new ReportGenerationException("Error generating report occurred in exportReportToExcel method because jasperPrint is null."); 96 | } 97 | 98 | try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { 99 | JRXlsxExporter exporter = new JRXlsxExporter(); 100 | 101 | exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); 102 | exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(byteArrayOutputStream)); 103 | 104 | exporter.exportReport(); 105 | return byteArrayOutputStream.toByteArray(); 106 | 107 | } catch (IOException e) { 108 | throw new ReportGenerationException("Error occurred while exporting report to Excel format", e); 109 | } 110 | } 111 | 112 | 113 | 114 | /** 115 | * Exports the generated report object received as parameter into Excel or PDF and 116 | * returns the binary content as a byte array. 117 | * 118 | * @param jasperPrint 119 | * @return byte array representing the resulting file content 120 | * @throws JRException 121 | */ 122 | public byte[] exportJasperPrintToByteArray(JasperPrint jasperPrint) throws JRException { 123 | 124 | 125 | if(jasperPrint == null) { 126 | throw new ReportGenerationException("Error generating report occurred in exportReportToExcel method because jasperPrint is null."); 127 | } 128 | 129 | 130 | String extension = Optional.ofNullable(jasperPrint.getName()) 131 | .map(name -> name.substring(name.lastIndexOf(".") + 1).toLowerCase()) 132 | .orElse(""); 133 | 134 | if (extension.equals("xlsx")) { 135 | return exportReportToExcel(jasperPrint); 136 | 137 | } else if (extension.equals("pdf")) { 138 | return JasperExportManager.exportReportToPdf(jasperPrint); 139 | 140 | } else { 141 | throw new ReportGenerationException("Currently '" + extension + "' format cannot be exported to byte[]."); 142 | } 143 | 144 | } 145 | 146 | /** 147 | * Combines multiple JasperPrint objects into a single PDF document. 148 | * Exports the combined content into a PDF and returns the resulting binary content as a byte array. 149 | * 150 | * @param jasperPrintList List of JasperPrint objects to be combined into a single PDF 151 | * @return byte array representing the combined PDF content 152 | * @throws JRException if there is an error during the PDF export process 153 | */ 154 | public byte[] combineAndExportPdf(List jasperPrintList) throws JRException { 155 | 156 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 157 | JRPdfExporter exporter = new JRPdfExporter(); 158 | exporter.setExporterInput(SimpleExporterInput.getInstance(jasperPrintList)); 159 | exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputStream)); 160 | SimplePdfExporterConfiguration configuration = new SimplePdfExporterConfiguration(); 161 | configuration.setCreatingBatchModeBookmarks(true); 162 | exporter.setConfiguration(configuration); 163 | exporter.exportReport(); 164 | 165 | return outputStream.toByteArray(); 166 | } 167 | 168 | 169 | /** 170 | * This method is used when we want to put a sub-report inside our generated file, and we have to specify a data source. 171 | * @param records 172 | * @return 173 | */ 174 | public JRBeanCollectionDataSource getSubReportDataSource(List records) { 175 | return new JRBeanCollectionDataSource(records); 176 | } 177 | 178 | 179 | /** 180 | * This method is used to export a report to byte array 181 | * 182 | * @param records 183 | * @param fileName 184 | * @param jrxmlFileLocation 185 | * @return report as a byte array 186 | * @throws JRException 187 | */ 188 | public byte[] exportReportToByteArray(List records, String fileName, String jrxmlFileLocation) throws JRException { 189 | 190 | JasperPrint jasperPrint = this.extractResultsToJasperPrint(records, fileName, jrxmlFileLocation); 191 | 192 | return this.getReportByteArray(jasperPrint); 193 | } 194 | 195 | 196 | /** 197 | * This method is used to export a report to byte array 198 | * and also provide jasper parameters (example: provide sub-report, additional variables etc.) 199 | * 200 | * @param recordsOfMainReport 201 | * @param jasperParameters 202 | * @param fileName 203 | * @param jrxmlFileLocation 204 | * @return report as a byte array 205 | * @throws JRException 206 | */ 207 | public byte[] exportReportToByteArray(List recordsOfMainReport, Map jasperParameters, String fileName, String jrxmlFileLocation) throws JRException { 208 | 209 | JasperPrint jasperPrint = this.extractResultsToJasperPrint(recordsOfMainReport, jasperParameters, fileName, jrxmlFileLocation); 210 | 211 | return this.getReportByteArray(jasperPrint); 212 | } 213 | 214 | 215 | private byte[] getReportByteArray(JasperPrint jasperPrint) throws JRException { 216 | 217 | try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.exportJasperPrintToByteArray(jasperPrint))) { 218 | 219 | byte[] bytes = new byte[byteArrayInputStream.available()]; 220 | int bytesRead = byteArrayInputStream.read(bytes); 221 | 222 | if (bytes.length == bytesRead) { 223 | return bytes; 224 | } else { 225 | throw new JRException("Error: Not all bytes were read"); 226 | } 227 | 228 | } catch (IOException e) { 229 | throw new JRException("Error converting report to byte array", e); 230 | } 231 | } 232 | 233 | /** 234 | * This method is used to zip a List and then return them as a byte[]. 235 | * 236 | * @param listOfJasperPrints 237 | * @return zipped list as a byte array 238 | * @throws IOException 239 | * @throws JRException 240 | */ 241 | public byte[] zipJasperPrintList(List listOfJasperPrints) throws IOException, JRException { 242 | 243 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 244 | ZipOutputStream zipFile = new ZipOutputStream(byteArrayOutputStream); 245 | 246 | for (int j = 0; j < listOfJasperPrints.size(); j++) { 247 | 248 | byte[] reportAsBytes = null; 249 | byte[] buffer = new byte[1024]; 250 | 251 | reportAsBytes = this.exportJasperPrintToByteArray(listOfJasperPrints.get(j)); 252 | 253 | ByteArrayInputStream fis = new ByteArrayInputStream(reportAsBytes); 254 | zipFile.putNextEntry(new ZipEntry(listOfJasperPrints.get(j).getName())); 255 | int length; 256 | 257 | while ((length = fis.read(buffer, 0, 1024)) > 0) { 258 | zipFile.write(buffer, 0, length); 259 | } 260 | 261 | zipFile.closeEntry(); 262 | 263 | // close the InputStream 264 | fis.close(); 265 | 266 | } 267 | 268 | zipFile.close(); 269 | return byteArrayOutputStream.toByteArray(); 270 | } 271 | 272 | 273 | } 274 | -------------------------------------------------------------------------------- /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 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.8 9 | 10 | 11 | com.ainigma100 12 | department-api 13 | 0.0.1-SNAPSHOT 14 | department-api 15 | department-api 16 | 17 | 18 | 21 19 | 1.6.3 20 | 0.2.0 21 | 2.8.13 22 | 6.21.5 23 | 5.4.1 24 | 0.8.13 25 | 3.10.0.2594 26 | 27 | **/dto/**, 28 | **/entity/**, 29 | **/exception/** 30 | 31 | 8.15.0 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-validation 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-web 49 | 50 | 51 | 52 | org.postgresql 53 | postgresql 54 | runtime 55 | 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | true 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | 70 | org.mapstruct 71 | mapstruct 72 | ${org.mapstruct.version} 73 | 74 | 75 | 76 | org.springdoc 77 | springdoc-openapi-starter-webmvc-ui 78 | ${springdoc-openapi-starter-webmvc-ui.version} 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-actuator 84 | 85 | 86 | 87 | 88 | net.sf.jasperreports 89 | jasperreports 90 | ${net.sf.jasperreports.version} 91 | 92 | 93 | net.sf.jasperreports 94 | jasperreports-fonts 95 | ${net.sf.jasperreports.version} 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-testcontainers 102 | test 103 | 104 | 105 | org.testcontainers 106 | junit-jupiter 107 | test 108 | 109 | 110 | org.testcontainers 111 | postgresql 112 | test 113 | 114 | 115 | 116 | 117 | com.h2database 118 | h2 119 | test 120 | 121 | 122 | 123 | org.springframework.boot 124 | spring-boot-starter-mail 125 | 126 | 127 | org.springframework.boot 128 | spring-boot-starter-thymeleaf 129 | 130 | 131 | 132 | org.apache.poi 133 | poi 134 | ${poi.version} 135 | test 136 | 137 | 138 | org.apache.poi 139 | poi-ooxml 140 | ${poi.version} 141 | test 142 | 143 | 144 | 145 | org.flywaydb 146 | flyway-core 147 | 148 | 149 | 150 | org.flywaydb 151 | flyway-database-postgresql 152 | 153 | 154 | 155 | com.bucket4j 156 | bucket4j_jdk17-core 157 | ${bucket4j_jdk17-core.version} 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | org.springframework.boot 166 | spring-boot-maven-plugin 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-compiler-plugin 171 | ${maven-compiler-plugin.version} 172 | 173 | ${java.version} 174 | ${java.version} 175 | 176 | 177 | org.mapstruct 178 | mapstruct-processor 179 | ${org.mapstruct.version} 180 | 181 | 182 | org.projectlombok 183 | lombok 184 | ${lombok.version} 185 | 186 | 187 | org.projectlombok 188 | lombok-mapstruct-binding 189 | ${lombok-mapstruct-binding.version} 190 | 191 | 192 | 193 | 194 | 195 | org.apache.maven.plugins 196 | maven-surefire-plugin 197 | 198 | true 199 | 200 | 201 | 202 | org.apache.maven.plugins 203 | maven-failsafe-plugin 204 | 205 | 206 | 207 | integration-test 208 | verify 209 | 210 | 211 | 212 | 213 | 214 | org.sonarsource.scanner.maven 215 | sonar-maven-plugin 216 | ${sonar-maven-plugin.version} 217 | 218 | 219 | org.jacoco 220 | jacoco-maven-plugin 221 | ${jacoco-maven-plugin.version} 222 | 223 | 224 | prepare-agent 225 | 226 | prepare-agent 227 | 228 | 229 | 230 | report 231 | 232 | report 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | unit-tests 243 | 244 | true 245 | 246 | 247 | 248 | 249 | org.apache.maven.plugins 250 | maven-surefire-plugin 251 | 252 | false 253 | unit 254 | 255 | 256 | 257 | 258 | 259 | 260 | integration-tests 261 | 262 | 263 | 264 | org.apache.maven.plugins 265 | maven-failsafe-plugin 266 | 267 | 268 | 269 | integration-test 270 | verify 271 | 272 | 273 | 274 | 275 | false 276 | 277 | **/*IntegrationTest.java 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /src/test/java/com/ainigma100/departmentapi/integration/ReportControllerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.ainigma100.departmentapi.integration; 2 | 3 | import com.ainigma100.departmentapi.entity.Department; 4 | import com.ainigma100.departmentapi.entity.Employee; 5 | import com.ainigma100.departmentapi.enums.ReportLanguage; 6 | import com.ainigma100.departmentapi.filter.RateLimitingFilter; 7 | import com.ainigma100.departmentapi.repository.DepartmentRepository; 8 | import com.ainigma100.departmentapi.repository.EmployeeRepository; 9 | import org.apache.poi.ss.usermodel.Cell; 10 | import org.apache.poi.ss.usermodel.Row; 11 | import org.apache.poi.xssf.usermodel.XSSFSheet; 12 | import org.apache.poi.xssf.usermodel.XSSFWorkbook; 13 | import org.junit.jupiter.api.*; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.test.context.ActiveProfiles; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | import org.springframework.test.web.servlet.ResultActions; 20 | import org.testcontainers.junit.jupiter.Testcontainers; 21 | 22 | import java.io.ByteArrayInputStream; 23 | import java.io.InputStream; 24 | import java.math.BigDecimal; 25 | import java.text.DecimalFormat; 26 | import java.text.NumberFormat; 27 | import java.util.Arrays; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | 31 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 32 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 34 | 35 | // Use random port for integration testing. the server will start on a random port 36 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 37 | @AutoConfigureMockMvc 38 | @Testcontainers(disabledWithoutDocker = true) 39 | @ActiveProfiles("testcontainers") 40 | @Tag("integration") 41 | class ReportControllerIntegrationTest extends AbstractContainerBaseTest { 42 | 43 | @Autowired 44 | private MockMvc mockMvc; 45 | 46 | @Autowired 47 | private EmployeeRepository employeeRepository; 48 | 49 | @Autowired 50 | private DepartmentRepository departmentRepository; 51 | 52 | @Autowired 53 | private RateLimitingFilter rateLimitingFilter; 54 | 55 | NumberFormat formatter ; 56 | 57 | private static List departmentList; 58 | private static List employeeList; 59 | 60 | @BeforeEach 61 | public void setup() { 62 | 63 | formatter = new DecimalFormat("#"); 64 | 65 | Department department = new Department(); 66 | department.setDepartmentCode( "ABC" ); 67 | department.setDepartmentName( "Department 1" ); 68 | department.setDepartmentDescription( "Description 1" ); 69 | 70 | Department department2 = new Department(); 71 | department2.setDepartmentCode( "FIN" ); 72 | department2.setDepartmentName( "Department 2" ); 73 | department2.setDepartmentDescription( "Description 2" ); 74 | 75 | departmentList = Arrays.asList( department, department2 ); 76 | 77 | Employee employee = new Employee(); 78 | employee.setFirstName( "John" ); 79 | employee.setLastName( "Wick" ); 80 | employee.setEmail( "jwick@gmail.com" ); 81 | employee.setSalary( BigDecimal.valueOf( 40_000_000 ) ); 82 | employee.setDepartment( department ); 83 | 84 | Employee employee2 = new Employee(); 85 | employee2.setFirstName( "Luffy" ); 86 | employee2.setLastName( "Monkey D." ); 87 | employee2.setEmail( "mluffy@gmail.com" ); 88 | employee2.setSalary( BigDecimal.valueOf( 50_000_000 ) ); 89 | employee2.setDepartment( department ); 90 | 91 | employeeList = Arrays.asList( employee, employee2 ); 92 | 93 | departmentRepository.deleteAll(); 94 | employeeRepository.deleteAll(); 95 | } 96 | 97 | @AfterEach 98 | void resetRateLimitBuckets() { 99 | rateLimitingFilter.clearBuckets(); 100 | } 101 | 102 | @Test 103 | @DisplayName("Generate an Excel report containing all the departments") 104 | void givenNoInput_whenGenerateDepartmentsExcelReport_thenReturnInputStreamResource() 105 | throws Exception { 106 | 107 | // given - precondition or setup 108 | departmentRepository.saveAll( departmentList ); 109 | employeeRepository.saveAll( employeeList ); 110 | 111 | // when - action or behaviour that we are going to test 112 | ResultActions response = mockMvc.perform( get( "/api/v1/reports/excel/departments" ) ); 113 | 114 | // then - verify the output 115 | response.andExpect( status().isOk() ); 116 | 117 | InputStream is = new ByteArrayInputStream( 118 | response.andReturn().getResponse().getContentAsByteArray() ); 119 | 120 | 121 | //Create Workbook instance holding reference to .xlsx file 122 | XSSFWorkbook workbook = new XSSFWorkbook( is ); 123 | 124 | //Get first/desired sheet from the workbook 125 | XSSFSheet sheet = workbook.getSheetAt( 0 ); 126 | 127 | //Iterate through each row one by one 128 | Iterator rowIterator = sheet.iterator(); 129 | 130 | 131 | // Skip rows that do not contain info given that I know the template of the xlsx 132 | skipRows( rowIterator, 5 ); 133 | 134 | for ( int i = 0; i < departmentList.size(); i++ ) { 135 | String departmentName = departmentList.get( i ).getDepartmentName(); 136 | Row row = rowIterator.next(); 137 | Iterator cellIterator = row.cellIterator(); 138 | cellIterator.next(); 139 | // id | Department Code | Department Name | Department Description | Total employee | 140 | int count = 0; 141 | while ( cellIterator.hasNext() ) { 142 | Cell cell = cellIterator.next(); 143 | //Check the cell type and format accordingly 144 | switch ( count ) { 145 | case 0: 146 | assertThat( cell.getStringCellValue() ).isEqualTo( departmentList.get( i ).getDepartmentCode() ); 147 | break; 148 | case 1: 149 | assertThat( cell.getStringCellValue() ).isEqualTo( departmentList.get( i ).getDepartmentName() ); 150 | break; 151 | case 2: 152 | assertThat( cell.getStringCellValue() ).isEqualTo( departmentList.get( i ).getDepartmentDescription() ); 153 | break; 154 | case 3: 155 | Long employeeDepartmentCounter = employeeList.stream() 156 | .filter( e -> e.getDepartment() 157 | .getDepartmentName() 158 | .equals( departmentName ) ) 159 | .count(); 160 | assertThat( (long) cell.getNumericCellValue() ).isEqualTo( 161 | employeeDepartmentCounter ); 162 | break; 163 | } 164 | count++; 165 | } 166 | } 167 | } 168 | 169 | 170 | @Test 171 | @DisplayName("Generate an Excel report containing all the employees") 172 | void givenNoInput_whenGenerateEmployeesExcelReport_thenReturnInputStreamResource() 173 | throws Exception { 174 | 175 | // given - precondition or setup 176 | departmentRepository.saveAll( departmentList ); 177 | employeeRepository.saveAll( employeeList ); 178 | 179 | // when - action or behaviour that we are going to test 180 | ResultActions response = mockMvc.perform( get( "/api/v1/reports/excel/employees" ) ); 181 | 182 | // then - verify the output 183 | response.andExpect( status().isOk() ); 184 | 185 | InputStream is = new ByteArrayInputStream( 186 | response.andReturn().getResponse().getContentAsByteArray() ); 187 | 188 | 189 | //Create Workbook instance holding reference to .xlsx file 190 | XSSFWorkbook workbook = new XSSFWorkbook( is ); 191 | 192 | //Get first/desired sheet from the workbook 193 | XSSFSheet sheet = workbook.getSheetAt( 0 ); 194 | 195 | //Iterate through each row one by one 196 | Iterator rowIterator = sheet.iterator(); 197 | 198 | // Skip rows that do not contain info given that I know the template of the xlsx 199 | skipRows( rowIterator, 3 ); 200 | 201 | Row TotalRecordsRow = rowIterator.next(); 202 | Iterator TotalRecordsCellRowIterator = TotalRecordsRow.cellIterator(); 203 | TotalRecordsCellRowIterator.next(); 204 | assertThat((int)TotalRecordsCellRowIterator.next().getNumericCellValue() ).isEqualTo( employeeList.size() ); 205 | 206 | 207 | skipRows( rowIterator, 1 ); 208 | 209 | for ( int i = 0; i < employeeList.size(); i++ ) { 210 | Row row = rowIterator.next(); 211 | Iterator cellIterator = row.cellIterator(); 212 | 213 | // FirstName | LastName | Email | Salary | 214 | int count = 0; 215 | while ( cellIterator.hasNext() ) { 216 | Cell cell = cellIterator.next(); 217 | //Check the cell type and format accordingly 218 | switch ( count ) { 219 | case 0: 220 | assertThat( cell.getStringCellValue() ).isEqualTo( employeeList.get( i ).getFirstName() ); 221 | break; 222 | case 1: 223 | assertThat( cell.getStringCellValue() ).isEqualTo( employeeList.get( i ).getLastName() ); 224 | break; 225 | case 2: 226 | assertThat( cell.getStringCellValue() ).isEqualTo( employeeList.get( i ).getEmail() ); 227 | break; 228 | case 3: 229 | assertThat( BigDecimal.valueOf( 230 | Long.parseLong( formatter.format(cell.getNumericCellValue() ) ) )).isEqualTo( employeeList.get( i ).getSalary() ); 231 | } 232 | count++; 233 | } 234 | } 235 | 236 | 237 | } 238 | 239 | @Test 240 | @DisplayName("Generate a PDF report containing all the departments along with all the employees in the specified language") 241 | void givenReportLanguage_whenGeneratePdfFullReport_thenReturnInputStreamResource() throws Exception { 242 | 243 | // given - precondition or setup 244 | ReportLanguage reportLanguage = ReportLanguage.EN; 245 | departmentRepository.saveAll( departmentList ); 246 | employeeRepository.saveAll( employeeList ); 247 | 248 | // when - action or behaviour that we are going to test 249 | ResultActions response = mockMvc.perform(get( "/api/v1/reports/pdf/full-report" ) 250 | .param("language", String.valueOf(reportLanguage))); 251 | 252 | // then - verify the output 253 | response 254 | // verify the status code that is returned 255 | .andExpect( status().isOk() ); 256 | 257 | } 258 | 259 | @Test 260 | @DisplayName("Generate a combined PDF report from two separate reports in the specified language") 261 | void givenReportLanguage_whenGenerateCombinedPdfReport_thenReturnInputStreamResource() throws Exception { 262 | 263 | // given - precondition or setup 264 | ReportLanguage reportLanguage = ReportLanguage.EN; 265 | departmentRepository.saveAll( departmentList ); 266 | employeeRepository.saveAll( employeeList ); 267 | 268 | // when - action or behaviour that we are going to test 269 | ResultActions response = mockMvc.perform(get( "/api/v1/reports/pdf/combined-report" ) 270 | .param("language", String.valueOf(reportLanguage))); 271 | 272 | // then - verify the output 273 | response 274 | // verify the status code that is returned 275 | .andExpect( status().isOk() ); 276 | 277 | } 278 | 279 | @Test 280 | @DisplayName("Generate a zip file which contains two excel reports") 281 | void givenNoInput_whenGenerateAndZipReports_thenReturnInputStreamResource() throws Exception { 282 | 283 | // given - precondition or setup 284 | departmentRepository.saveAll( departmentList ); 285 | employeeRepository.saveAll( employeeList ); 286 | 287 | // when - action or behaviour that we are going to test 288 | ResultActions response = mockMvc.perform( get( "/api/v1/reports/zip" ) ); 289 | 290 | // then - verify the output 291 | response 292 | // verify the status code that is returned 293 | .andExpect( status().isOk() ); 294 | 295 | } 296 | 297 | @Test 298 | @DisplayName("Generate a multi-sheet Excel report containing departments and employees") 299 | void givenNoInput_whenGenerateMultiSheetExcelReport_thenReturnInputStreamResource() 300 | throws Exception { 301 | 302 | // given - precondition or setup 303 | departmentRepository.saveAll( departmentList ); 304 | employeeRepository.saveAll( employeeList ); 305 | 306 | // when - action or behaviour that we are going to test 307 | ResultActions response = mockMvc.perform( get( "/api/v1/reports/multi-sheet-excel" ) ); 308 | 309 | // then - verify the output 310 | response 311 | // verify the status code that is returned 312 | .andExpect( status().isOk() ); 313 | 314 | } 315 | 316 | private void skipRows(Iterator iterator, int num ) { 317 | for ( int i = 0; i < num; i++ ) { 318 | iterator.next(); 319 | } 320 | } 321 | 322 | } 323 | --------------------------------------------------------------------------------