From f207e24b2c36bde2e05529f422ac4d01b02114cd Mon Sep 17 00:00:00 2001 From: danishahsancs Date: Sun, 7 Sep 2025 14:52:27 -0400 Subject: [PATCH] fixed bugs --- .vscode/settings.json | 3 + pom.xml | 46 +++-- .../PersistenceStarterApplication.java | 11 +- .../controller/DepartmentController.java | 66 +++++++ .../controller/EmployeeController.java | 75 +++++++ .../controller/ManagerController.java | 47 +++++ .../persistenceapp/domain/Department.java | 94 +++++++++ .../persistenceapp/domain/Employee.java | 183 ++++++++++++++++++ .../persistenceapp/exception/ErrorDetail.java | 26 +++ .../exception/ResourceNotFoundException.java | 11 ++ .../exception/RestExceptionHandler.java | 55 ++++++ .../exception/ValidationError.java | 16 ++ .../repository/DepartmentRepository.java | 12 ++ .../repository/EmployeeRepository.java | 25 +++ .../services/DepartmentService.java | 78 ++++++++ .../services/EmployeeService.java | 173 +++++++++++++++++ src/main/resources/application-h2.properties | 4 - src/main/resources/application.properties | 44 ++++- src/main/resources/data.sql | 37 ++++ src/main/resources/schema-h2.sql | 33 ---- .../PersistenceStarterApplicationTests.java | 7 +- 21 files changed, 980 insertions(+), 66 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/main/java/io/zipcoder/persistenceapp/controller/DepartmentController.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/controller/EmployeeController.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/controller/ManagerController.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/domain/Department.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/domain/Employee.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/exception/ErrorDetail.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/exception/ResourceNotFoundException.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/exception/RestExceptionHandler.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/exception/ValidationError.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/repository/DepartmentRepository.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/repository/EmployeeRepository.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/services/DepartmentService.java create mode 100644 src/main/java/io/zipcoder/persistenceapp/services/EmployeeService.java delete mode 100644 src/main/resources/application-h2.properties create mode 100644 src/main/resources/data.sql delete mode 100644 src/main/resources/schema-h2.sql diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 274f418..7be85e9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,57 +1,76 @@ - + 4.0.0 io.zipcoder - persistence-starter + employee-directory 0.0.1-SNAPSHOT jar - persistence-starter - Spring Boot Starter project for persistence projects + Employee Directory + Spring Boot Persistence Project: Employee Directory Application org.springframework.boot spring-boot-starter-parent - 1.5.3.RELEASE - + 2.7.18 + + 11 UTF-8 UTF-8 - 1.8 + org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot - spring-boot-starter-jdbc - + + org.springframework.boot spring-boot-starter-web + com.h2database h2 - compile + runtime + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot spring-boot-starter-test test + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + org.springframework.boot spring-boot-maven-plugin @@ -59,5 +78,4 @@ - - + \ No newline at end of file diff --git a/src/main/java/io/zipcoder/persistenceapp/PersistenceStarterApplication.java b/src/main/java/io/zipcoder/persistenceapp/PersistenceStarterApplication.java index 1f1e185..e43e081 100644 --- a/src/main/java/io/zipcoder/persistenceapp/PersistenceStarterApplication.java +++ b/src/main/java/io/zipcoder/persistenceapp/PersistenceStarterApplication.java @@ -1,10 +1,9 @@ package io.zipcoder.persistenceapp; -import org.h2.server.web.WebServlet; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.ServletRegistrationBean; -import org.springframework.context.annotation.Bean; + @SpringBootApplication public class PersistenceStarterApplication { @@ -13,10 +12,4 @@ public static void main(String[] args) { SpringApplication.run(PersistenceStarterApplication.class, args); } - @Bean - ServletRegistrationBean h2servletRegistration(){ - ServletRegistrationBean registrationBean = new ServletRegistrationBean( new WebServlet()); - registrationBean.addUrlMappings("/console/*"); - return registrationBean; - } } diff --git a/src/main/java/io/zipcoder/persistenceapp/controller/DepartmentController.java b/src/main/java/io/zipcoder/persistenceapp/controller/DepartmentController.java new file mode 100644 index 0000000..7920d1c --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/controller/DepartmentController.java @@ -0,0 +1,66 @@ +package io.zipcoder.persistenceapp.controller; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.zipcoder.persistenceapp.domain.Department; +import io.zipcoder.persistenceapp.domain.Employee; +import io.zipcoder.persistenceapp.services.DepartmentService; +import io.zipcoder.persistenceapp.services.EmployeeService; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/API/departments") +public class DepartmentController { + + private final DepartmentService departments; + private final EmployeeService employees; + + public DepartmentController(DepartmentService departments, EmployeeService employees) { + this.departments = departments; + this.employees = employees; + } + + // Create a Department + @PostMapping + public ResponseEntity create(@Valid @RequestBody Department d) { + return new ResponseEntity<>(departments.create(d), HttpStatus.CREATED); + } + + // Set a new department manager + @PutMapping("/{id}/manager/{managerEmpId}") + public ResponseEntity setManager(@PathVariable Long id, @PathVariable Long managerEmpId) { + return ResponseEntity.ok(departments.setManager(id, managerEmpId)); + } + + // Change department name + @PutMapping("/{id}/name") + public ResponseEntity rename(@PathVariable Long id, @RequestParam("value") String newName) { + return ResponseEntity.ok(departments.rename(id, newName)); + } + + // Get all employees of a department + @GetMapping("/{id}/employees") + public ResponseEntity> listEmployees(@PathVariable Long id) { + return ResponseEntity.ok(employees.getByDepartment(id)); + } + + // Remove all employees from a department + @DeleteMapping("/{id}/employees") + public ResponseEntity deleteAllInDepartment(@PathVariable Long id) { + employees.deleteByDepartment(id); + return ResponseEntity.ok().build(); + } + + // Merge departments by names: /API/departments/merge?from=B&to=A + @PostMapping("/merge") + public ResponseEntity mergeByNames(@RequestParam("from") String fromName, + @RequestParam("to") String toName) { + departments.mergeByNames(fromName, toName); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/controller/EmployeeController.java b/src/main/java/io/zipcoder/persistenceapp/controller/EmployeeController.java new file mode 100644 index 0000000..c06db22 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/controller/EmployeeController.java @@ -0,0 +1,75 @@ +package io.zipcoder.persistenceapp.controller; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.zipcoder.persistenceapp.domain.Employee; +import io.zipcoder.persistenceapp.services.EmployeeService; + +import javax.validation.Valid; +import java.util.*; + +@RestController +@RequestMapping("/API/employees") +public class EmployeeController { + + private final EmployeeService employees; + + public EmployeeController(EmployeeService employees) { + this.employees = employees; + } + + // Create employee + @PostMapping + public ResponseEntity createEmployee(@Valid @RequestBody Employee e) { + Employee saved = employees.create(e); + return new ResponseEntity<>(saved, HttpStatus.CREATED); + } + + // Update employee fields (partial) + @PutMapping("/{id}") + public ResponseEntity updateEmployee(@PathVariable Long id, @RequestBody Employee patch) { + Employee updated = employees.update(id, patch); + return new ResponseEntity<>(updated, HttpStatus.OK); + } + + // Set manager + @PutMapping("/{id}/manager/{managerId}") + public ResponseEntity setManager(@PathVariable Long id, @PathVariable Long managerId) { + Employee updated = employees.setManager(id, managerId); + return new ResponseEntity<>(updated, HttpStatus.OK); + } + + // Get attributes of a particular employee (full object) + @GetMapping("/{id}") + public ResponseEntity getEmployee(@PathVariable Long id) { + return ResponseEntity.ok(employees.getEmployee(id)); + } + + // Get hierarchy upwards (manager, manager's manager, ...) + @GetMapping("/{id}/manager-chain") + public ResponseEntity> getManagerChain(@PathVariable Long id) { + return ResponseEntity.ok(employees.getManagerChain(id)); + } + + // List employees with no assigned manager + @GetMapping("/unmanaged") + public ResponseEntity> unmanaged() { + return ResponseEntity.ok(employees.getUnmanaged()); + } + + // GET /api/employees/{id}/hierarchy + @GetMapping("/{id}/hierarchy") + public List getHierarchy(@PathVariable Long id) { + return employees.getHierarchy(id); + } + + // Remove a particular employee or list via query param: /API/employees?ids=1,2,3 + @DeleteMapping + public ResponseEntity deleteByIds(@RequestParam("ids") List ids) { + employees.deleteEmployeesByIds(ids); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/controller/ManagerController.java b/src/main/java/io/zipcoder/persistenceapp/controller/ManagerController.java new file mode 100644 index 0000000..d75faa6 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/controller/ManagerController.java @@ -0,0 +1,47 @@ +package io.zipcoder.persistenceapp.controller; + + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.zipcoder.persistenceapp.domain.Employee; +import io.zipcoder.persistenceapp.services.EmployeeService; + +import java.util.List; + +@RestController +@RequestMapping("/API/managers") +public class ManagerController { + + private final EmployeeService employees; + + public ManagerController(EmployeeService employees) { + this.employees = employees; + } + + // Get direct reports + @GetMapping("/{id}/reports") + public ResponseEntity> directReports(@PathVariable Long id) { + return ResponseEntity.ok(employees.getDirectReports(id)); + } + + // Get direct+indirect reports: /API/managers/{id}/reports/all + @GetMapping("/{id}/reports/all") + public ResponseEntity> allReports(@PathVariable Long id) { + return ResponseEntity.ok(employees.getAllReportsRecursive(id)); + } + + // Remove all employees under a manager (including indirect) + @DeleteMapping("/{id}/reports/all") + public ResponseEntity deleteAllUnder(@PathVariable Long id) { + employees.deleteAllUnderManagerRecursive(id); + return ResponseEntity.ok().build(); + } + + // Remove only direct reports and reassign their reports upwards + @DeleteMapping("/{id}/reports") + public ResponseEntity deleteDirectReports(@PathVariable Long id) { + employees.deleteDirectReports(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/domain/Department.java b/src/main/java/io/zipcoder/persistenceapp/domain/Department.java new file mode 100644 index 0000000..ef20e47 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/domain/Department.java @@ -0,0 +1,94 @@ +// package io.zipcoder.persistenceapp.domain; + +// import com.fasterxml.jackson.annotation.JsonIgnore; +// import javax.persistence.*; +// import javax.validation.constraints.NotNull; + +// import java.util.HashSet; +// import java.util.Set; + +// @Entity +// @Table(name = "department") +// public class Department { + +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// @Column(name = "dpt_num") +// private Long id; + +// @NotNull(message = "{NotEmpty.department.name}") +// @Column(name = "dpt_name", nullable = false, unique = true) +// private String name; + +// // Department manager is an employee; nullable until assigned +// @OneToOne +// @JoinColumn(name = "manager_emp_num") +// private Employee manager; + +// // bi-directional convenience (not needed for persistence ops, but handy for reads) +// @OneToMany(mappedBy = "department") +// @JsonIgnore // avoid large recursive payloads by default +// private Set employees = new HashSet<>(); + +// // getters/setters +// public Long getId() { return id; } +// public void setId(Long id) { this.id = id; } + +// public String getName() { return name; } +// public void setName(String name) { this.name = name; } + +// public Employee getManager() { return manager; } +// public void setManager(Employee manager) { this.manager = manager; } + +// public Set getEmployees() { return employees; } +// public void setEmployees(Set employees) { this.employees = employees; } +// } + + +package io.zipcoder.persistenceapp.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "department") +public class Department { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "dpt_num") + private Long id; + + @NotNull(message = "{NotEmpty.department.name}") + @Column(name = "dpt_name", nullable = false, unique = true) + private String name; + + // Department manager is an employee; nullable until assigned + @OneToOne + @JoinColumn(name = "manager_emp_num") + @JsonIgnoreProperties({"manager", "directReports", "department"}) // Only show basic manager info + private Employee manager; + + // bi-directional convenience (not needed for persistence ops, but handy for reads) + @OneToMany(mappedBy = "department") + @JsonIgnore // Always avoid large recursive payloads by default + private Set employees = new HashSet<>(); + + // getters/setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Employee getManager() { return manager; } + public void setManager(Employee manager) { this.manager = manager; } + + public Set getEmployees() { return employees; } + public void setEmployees(Set employees) { this.employees = employees; } +} \ No newline at end of file diff --git a/src/main/java/io/zipcoder/persistenceapp/domain/Employee.java b/src/main/java/io/zipcoder/persistenceapp/domain/Employee.java new file mode 100644 index 0000000..c407f2d --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/domain/Employee.java @@ -0,0 +1,183 @@ +// package io.zipcoder.persistenceapp.domain; + +// import com.fasterxml.jackson.annotation.JsonIgnore; +// import javax.persistence.*; +// import javax.validation.constraints.*; +// import java.time.LocalDate; +// import java.util.HashSet; +// import java.util.Set; + +// @Entity +// @Table(name = "employee") +// public class Employee { + +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// @Column(name = "emp_num") +// private Long id; + +// @NotEmpty(message = "{NotEmpty.employee.firstName}") +// @Column(name = "first_name", nullable = false) +// private String firstName; + +// @NotEmpty(message = "{NotEmpty.employee.lastName}") +// @Column(name = "last_name", nullable = false) +// private String lastName; + +// @NotEmpty(message = "{NotEmpty.employee.title}") +// @Column(name = "title", nullable = false) +// private String title; + +// @NotEmpty(message = "{NotEmpty.employee.phone}") +// @Column(name = "phone", nullable = false) +// private String phone; + +// @Email(message = "{Email.employee.email}") +// @NotEmpty(message = "{NotEmpty.employee.email}") +// @Column(name = "email", nullable = false, unique = true) +// private String email; + +// @PastOrPresent(message = "{PastOrPresent.employee.hireDate}") +// @Column(name = "hire_date", nullable = false) +// private LocalDate hireDate; + +// // self-referencing manager relationship +// @ManyToOne +// @JoinColumn(name = "manager_emp_num") +// private Employee manager; + +// // inverse side for convenience +// @OneToMany(mappedBy = "manager") +// @JsonIgnore +// private Set directReports = new HashSet<>(); + +// // department assignment +// @ManyToOne +// @JoinColumn(name = "dpt_num") +// private Department department; + +// // getters/setters +// public Long getId() { return id; } +// public void setId(Long id) { this.id = id; } + +// public String getFirstName() { return firstName; } +// public void setFirstName(String firstName) { this.firstName = firstName; } + +// public String getLastName() { return lastName; } +// public void setLastName(String lastName) { this.lastName = lastName; } + +// public String getTitle() { return title; } +// public void setTitle(String title) { this.title = title; } + +// public String getPhone() { return phone; } +// public void setPhone(String phone) { this.phone = phone; } + +// public String getEmail() { return email; } +// public void setEmail(String email) { this.email = email; } + +// public LocalDate getHireDate() { return hireDate; } +// public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; } + +// public Employee getManager() { return manager; } +// public void setManager(Employee manager) { this.manager = manager; } + +// public Set getDirectReports() { return directReports; } +// public void setDirectReports(Set directReports) { this.directReports = directReports; } + +// public Department getDepartment() { return department; } +// public void setDepartment(Department department) { this.department = department; } +// } + + +package io.zipcoder.persistenceapp.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import javax.persistence.*; +import javax.validation.constraints.*; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "emp_num") + private Long id; + + @NotEmpty(message = "{NotEmpty.employee.firstName}") + @Column(name = "first_name", nullable = false) + private String firstName; + + @NotEmpty(message = "{NotEmpty.employee.lastName}") + @Column(name = "last_name", nullable = false) + private String lastName; + + @NotEmpty(message = "{NotEmpty.employee.title}") + @Column(name = "title", nullable = false) + private String title; + + @NotEmpty(message = "{NotEmpty.employee.phone}") + @Column(name = "phone", nullable = false) + private String phone; + + @Email(message = "{Email.employee.email}") + @NotEmpty(message = "{NotEmpty.employee.email}") + @Column(name = "email", nullable = false, unique = true) + private String email; + + @PastOrPresent(message = "{PastOrPresent.employee.hireDate}") + @Column(name = "hire_date", nullable = false) + private LocalDate hireDate; + + // self-referencing manager relationship + @ManyToOne + @JoinColumn(name = "manager_emp_num") + @JsonIgnoreProperties({"manager", "directReports", "department"}) // Prevent deep nesting + private Employee manager; + + // inverse side for convenience + @OneToMany(mappedBy = "manager") + @JsonIgnore // Always ignore to prevent circular references + private Set directReports = new HashSet<>(); + + // department assignment + @ManyToOne + @JoinColumn(name = "dpt_num") + @JsonIgnoreProperties({"employees", "manager"}) // Only show basic department info + private Department department; + + // getters/setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public LocalDate getHireDate() { return hireDate; } + public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; } + + public Employee getManager() { return manager; } + public void setManager(Employee manager) { this.manager = manager; } + + public Set getDirectReports() { return directReports; } + public void setDirectReports(Set directReports) { this.directReports = directReports; } + + public Department getDepartment() { return department; } + public void setDepartment(Department department) { this.department = department; } +} \ No newline at end of file diff --git a/src/main/java/io/zipcoder/persistenceapp/exception/ErrorDetail.java b/src/main/java/io/zipcoder/persistenceapp/exception/ErrorDetail.java new file mode 100644 index 0000000..b687de1 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/exception/ErrorDetail.java @@ -0,0 +1,26 @@ +package io.zipcoder.persistenceapp.exception; + +import java.util.List; +import java.util.Map; + +public class ErrorDetail { + private String title; + private int status; + private String detail; + private long timeStamp; + private String developerMessage; + private Map> errors; + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public int getStatus() { return status; } + public void setStatus(int status) { this.status = status; } + public String getDetail() { return detail; } + public void setDetail(String detail) { this.detail = detail; } + public long getTimeStamp() { return timeStamp; } + public void setTimeStamp(long timeStamp) { this.timeStamp = timeStamp; } + public String getDeveloperMessage() { return developerMessage; } + public void setDeveloperMessage(String developerMessage) { this.developerMessage = developerMessage; } + public Map> getErrors() { return errors; } + public void setErrors(Map> errors) { this.errors = errors; } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/exception/ResourceNotFoundException.java b/src/main/java/io/zipcoder/persistenceapp/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..44a7fbc --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/exception/ResourceNotFoundException.java @@ -0,0 +1,11 @@ +package io.zipcoder.persistenceapp.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException() { super(); } + public ResourceNotFoundException(String message) { super(message); } + public ResourceNotFoundException(String message, Throwable cause) { super(message, cause); } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/exception/RestExceptionHandler.java b/src/main/java/io/zipcoder/persistenceapp/exception/RestExceptionHandler.java new file mode 100644 index 0000000..9d44458 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/exception/RestExceptionHandler.java @@ -0,0 +1,55 @@ +package io.zipcoder.persistenceapp.exception; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.http.*; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +@ControllerAdvice +public class RestExceptionHandler { + + @Autowired + private MessageSource messageSource; + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, HttpServletRequest req) { + ErrorDetail ed = new ErrorDetail(); + ed.setTitle("Resource Not Found"); + ed.setStatus(HttpStatus.NOT_FOUND.value()); + ed.setDetail(ex.getMessage()); + ed.setTimeStamp(new Date().getTime()); + ed.setDeveloperMessage(ex.getClass().getName()); + return new ResponseEntity<>(ed, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationError(MethodArgumentNotValidException manve, HttpServletRequest req) { + ErrorDetail ed = new ErrorDetail(); + ed.setTitle("Validation Failed"); + ed.setStatus(HttpStatus.BAD_REQUEST.value()); + ed.setDetail("Input validation failed"); + ed.setTimeStamp(new Date().getTime()); + ed.setDeveloperMessage(manve.getClass().getName()); + ed.setErrors(new HashMap<>()); + + List fieldErrors = manve.getBindingResult().getFieldErrors(); + for (FieldError fe : fieldErrors) { + List list = ed.getErrors().get(fe.getField()); + if (list == null) { + list = new ArrayList<>(); + ed.getErrors().put(fe.getField(), list); + } + ValidationError ve = new ValidationError(); + ve.setCode(fe.getCode()); + ve.setMessage(messageSource.getMessage(fe, null)); + list.add(ve); + } + return new ResponseEntity<>(ed, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/exception/ValidationError.java b/src/main/java/io/zipcoder/persistenceapp/exception/ValidationError.java new file mode 100644 index 0000000..39ee324 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/exception/ValidationError.java @@ -0,0 +1,16 @@ +package io.zipcoder.persistenceapp.exception; + +public class ValidationError { + private String code; + private String message; + + public ValidationError() {} + public ValidationError(String code, String message) { + this.code = code; + this.message = message; + } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/repository/DepartmentRepository.java b/src/main/java/io/zipcoder/persistenceapp/repository/DepartmentRepository.java new file mode 100644 index 0000000..c1b6bb4 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/repository/DepartmentRepository.java @@ -0,0 +1,12 @@ +package io.zipcoder.persistenceapp.repository; + + +import org.springframework.data.jpa.repository.JpaRepository; + +import io.zipcoder.persistenceapp.domain.Department; + +import java.util.Optional; + +public interface DepartmentRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/io/zipcoder/persistenceapp/repository/EmployeeRepository.java b/src/main/java/io/zipcoder/persistenceapp/repository/EmployeeRepository.java new file mode 100644 index 0000000..d3307f9 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/repository/EmployeeRepository.java @@ -0,0 +1,25 @@ +package io.zipcoder.persistenceapp.repository; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import io.zipcoder.persistenceapp.domain.Employee; + +import java.util.List; +import java.util.Optional; + +public interface EmployeeRepository extends JpaRepository { + + List findByManager_Id(Long managerId); + + List findByManagerIsNull(); + + List findByDepartment_Id(Long departmentId); + + Optional findByEmail(String email); + + // fetch hierarchy upwards (manager chain) with JPQL (single hop per query; service will loop) + @Query("select e.manager from Employee e where e.id = ?1") + Optional findManagerOf(Long employeeId); +} diff --git a/src/main/java/io/zipcoder/persistenceapp/services/DepartmentService.java b/src/main/java/io/zipcoder/persistenceapp/services/DepartmentService.java new file mode 100644 index 0000000..5c44288 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/services/DepartmentService.java @@ -0,0 +1,78 @@ +package io.zipcoder.persistenceapp.services; + + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import io.zipcoder.persistenceapp.domain.Department; +import io.zipcoder.persistenceapp.domain.Employee; +import io.zipcoder.persistenceapp.exception.ResourceNotFoundException; +import io.zipcoder.persistenceapp.repository.DepartmentRepository; +import io.zipcoder.persistenceapp.repository.EmployeeRepository; + +import java.util.List; + +@Service +@Transactional +public class DepartmentService { + + private final DepartmentRepository departmentRepo; + private final EmployeeRepository employeeRepo; + + public DepartmentService(DepartmentRepository departmentRepo, EmployeeRepository employeeRepo) { + this.departmentRepo = departmentRepo; + this.employeeRepo = employeeRepo; + } + + public Department create(Department d) { + return departmentRepo.save(d); + } + + public Department setManager(Long dptId, Long managerEmpId) { + Department d = getById(dptId); + Employee mgr = employeeRepo.findById(managerEmpId) + .orElseThrow(() -> new ResourceNotFoundException("Employee " + managerEmpId + " not found")); + d.setManager(mgr); + // ensure manager is in department + mgr.setDepartment(d); + mgr.setManager(mgr.getManager()); // no change; just explicit write-through + employeeRepo.save(mgr); + return departmentRepo.save(d); + } + + public Department rename(Long dptId, String newName) { + Department d = getById(dptId); + d.setName(newName); + return departmentRepo.save(d); + } + + public Department getById(Long id) { + return departmentRepo.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Department " + id + " not found")); + } + + public void mergeByNames(String fromName, String toName) { + Department from = departmentRepo.findByName(fromName) + .orElseThrow(() -> new ResourceNotFoundException("Department '" + fromName + "' not found")); + Department to = departmentRepo.findByName(toName) + .orElseThrow(() -> new ResourceNotFoundException("Department '" + toName + "' not found")); + + // move manager of B to report to manager of A + Employee mgrFrom = from.getManager(); + Employee mgrTo = to.getManager(); // may be null in edge cases + if (mgrFrom != null) { + mgrFrom.setManager(mgrTo); + mgrFrom.setDepartment(to); + employeeRepo.save(mgrFrom); + } + + // move all other employees to department A + List movers = employeeRepo.findByDepartment_Id(from.getId()); + for (Employee e : movers) { + e.setDepartment(to); + employeeRepo.save(e); + } + + // optional: delete old department or keep it; here we keep but empty + } +} diff --git a/src/main/java/io/zipcoder/persistenceapp/services/EmployeeService.java b/src/main/java/io/zipcoder/persistenceapp/services/EmployeeService.java new file mode 100644 index 0000000..635e321 --- /dev/null +++ b/src/main/java/io/zipcoder/persistenceapp/services/EmployeeService.java @@ -0,0 +1,173 @@ +package io.zipcoder.persistenceapp.services; + + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import io.zipcoder.persistenceapp.domain.Department; +import io.zipcoder.persistenceapp.domain.Employee; +import io.zipcoder.persistenceapp.exception.ResourceNotFoundException; +import io.zipcoder.persistenceapp.repository.DepartmentRepository; +import io.zipcoder.persistenceapp.repository.EmployeeRepository; + +import java.util.*; + +@Service +@Transactional +public class EmployeeService { + + private final EmployeeRepository employeeRepo; + private final DepartmentRepository departmentRepo; + + public EmployeeService(EmployeeRepository employeeRepo, DepartmentRepository departmentRepo) { + this.employeeRepo = employeeRepo; + this.departmentRepo = departmentRepo; + } + + public Employee create(Employee e) { + // if manager present, inherit manager's department + if (e.getManager() != null) { + Employee mgr = getEmployee(e.getManager().getId()); + e.setManager(mgr); + e.setDepartment(mgr.getDepartment()); + } + // if department provided without manager, keep as is + return employeeRepo.save(e); + } + + public Employee getEmployee(Long id) { + return employeeRepo.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Employee " + id + " not found")); + } + + public Employee update(Long id, Employee patch) { + Employee existing = getEmployee(id); + if (patch.getFirstName() != null) existing.setFirstName(patch.getFirstName()); + if (patch.getLastName() != null) existing.setLastName(patch.getLastName()); + if (patch.getTitle() != null) existing.setTitle(patch.getTitle()); + if (patch.getPhone() != null) existing.setPhone(patch.getPhone()); + if (patch.getEmail() != null) existing.setEmail(patch.getEmail()); + if (patch.getHireDate() != null) existing.setHireDate(patch.getHireDate()); + if (patch.getDepartment() != null) { + Department d = departmentRepo.findById(patch.getDepartment().getId()) + .orElseThrow(() -> new ResourceNotFoundException("Department " + patch.getDepartment().getId() + " not found")); + existing.setDepartment(d); + } + return employeeRepo.save(existing); + } + + public Employee setManager(Long empId, Long managerId) { + Employee emp = getEmployee(empId); + Employee mgr = getEmployee(managerId); + emp.setManager(mgr); + // rule: assigning a manager also moves the employee into manager’s department + emp.setDepartment(mgr.getDepartment()); + return employeeRepo.save(emp); + } + + public List getDirectReports(Long managerId) { + getEmployee(managerId); // verify exists + return employeeRepo.findByManager_Id(managerId); + } + + public List getAllReportsRecursive(Long managerId) { + getEmployee(managerId); // verify exists + List out = new ArrayList<>(); + Deque q = new ArrayDeque<>(); + q.add(managerId); + while (!q.isEmpty()) { + Long current = q.removeFirst(); + List direct = employeeRepo.findByManager_Id(current); + for (Employee e : direct) { + out.add(e); + q.add(e.getId()); + } + } + return out; + } + + // Replace your getManagerChain method with this fixed version: + +public List getManagerChain(Long empId) { + Employee e = getEmployee(empId); + List chain = new ArrayList<>(); + Set visited = new HashSet<>(); // Prevent infinite loops + + Long currentId = empId; + while (currentId != null && !visited.contains(currentId)) { + visited.add(currentId); + + // Use the repository query to get the manager + Optional managerOpt = employeeRepo.findManagerOf(currentId); + if (managerOpt.isPresent()) { + Employee manager = managerOpt.get(); + chain.add(manager); + currentId = manager.getId(); + } else { + break; // No more managers up the chain + } + } + return chain; +} + + public List getUnmanaged() { + return employeeRepo.findByManagerIsNull(); + } + + public List getByDepartment(Long dptId) { + return employeeRepo.findByDepartment_Id(dptId); + } + + public void deleteEmployeesByIds(List ids) { + ids.forEach(this::deleteEmployeeReassignReportsUpwards); + } + + public void deleteByDepartment(Long dptId) { + departmentRepo.findById(dptId) + .orElseThrow(() -> new ResourceNotFoundException("Department " + dptId + " not found")); + List inDept = employeeRepo.findByDepartment_Id(dptId); + inDept.forEach(this::deleteEmployeeReassignReportsUpwards); + } + + public void deleteAllUnderManagerRecursive(Long managerId) { + // deletes every descendant (but not the manager) + List all = getAllReportsRecursive(managerId); + all.forEach(this::deleteEmployeeReassignReportsUpwards); + } + + public void deleteDirectReports(Long managerId) { + List direct = getDirectReports(managerId); + for (Employee child : direct) { + deleteEmployeeReassignReportsUpwards(child); + } + } + + private void deleteEmployeeReassignReportsUpwards(Employee toDelete) { + // reassign their direct reports to the next manager up + Employee nextManager = toDelete.getManager(); + List direct = employeeRepo.findByManager_Id(toDelete.getId()); + for (Employee child : direct) { + child.setManager(nextManager); + // inherit department from new manager (if any) + child.setDepartment(nextManager != null ? nextManager.getDepartment() : child.getDepartment()); + employeeRepo.save(child); + } + employeeRepo.deleteById(toDelete.getId()); + } + + private void deleteEmployeeReassignReportsUpwards(Long empId) { + Employee e = getEmployee(empId); + deleteEmployeeReassignReportsUpwards(e); + } + + public List getHierarchy(Long empId) { + List chain = new ArrayList<>(); + Employee current = employeeRepo.findById(empId) + .orElseThrow(() -> new RuntimeException("Employee not found")); + while (current.getManager() != null) { + current = current.getManager(); + chain.add(current); + } + return chain; // ordered bottom → top + } +} diff --git a/src/main/resources/application-h2.properties b/src/main/resources/application-h2.properties deleted file mode 100644 index 74765cc..0000000 --- a/src/main/resources/application-h2.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.url=jdbc:h2:mem:testdb;Mode=Oracle -spring.datasource.platform=h2 -spring.jpa.hibernate.ddl-auto=none -spring.datasource.continue-on-error=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7d4dc6f..f98311e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,41 @@ -spring.profiles.active=h2 -logging.level.org.springframework.boot.context.embedded=INFO -spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect \ No newline at end of file +# ------------------------ +# H2 Database (in-memory) +# ------------------------ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=Oracle +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# ------------------------ +# JPA / Hibernate +# ------------------------ +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.properties.hibernate.format_sql=true + +# ------------------------ +# SQL Initialization - CRITICAL SETTINGS +# ------------------------ +spring.sql.init.mode=always +spring.sql.init.continue-on-error=false +spring.sql.init.platform=h2 +# Ensure data runs after schema creation +spring.jpa.defer-datasource-initialization=true + +# ------------------------ +# Logging to debug issues (MORE VERBOSE) +# ------------------------ +logging.level.org.springframework.jdbc.datasource.init=DEBUG +logging.level.org.springframework.jdbc.datasource.init.ScriptUtils=DEBUG +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +logging.level.root=INFO +logging.level.org.springframework.boot.autoconfigure.sql.init=DEBUG + + +# Prevent infinite recursion in JSON serialization +spring.jackson.serialization.fail-on-empty-beans=false +spring.jackson.serialization.write-dates-as-timestamps=false \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..6140004 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,37 @@ +-- Create this file as src/main/resources/data.sql +-- Delete or rename your import.sql file + +-- Insert Departments first (without managers) +INSERT INTO DEPARTMENT (DPT_NAME) VALUES ('Engineering'); +INSERT INTO DEPARTMENT (DPT_NAME) VALUES ('Sales'); +INSERT INTO DEPARTMENT (DPT_NAME) VALUES ('Recruiting'); + +-- Insert Employees +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Alice', 'Anders', 'Director of Engineering', '555-1000', 'alice@corp.com', '2016-01-10', NULL, 1); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Eli', 'Evans', 'VP Sales', '555-2000', 'eli@corp.com', '2015-03-12', NULL, 2); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Hana', 'Hughes', 'Recruiter', '555-3000', 'hana@corp.com', '2022-09-01', NULL, 3); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Bob', 'Baker', 'Engineering Manager', '555-1001', 'bob@corp.com', '2017-02-15', 1, 1); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Cara', 'Cole', 'Senior Engineer', '555-1002', 'cara@corp.com', '2019-04-01', 2, 1); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Derek', 'Diaz', 'Engineer', '555-1003', 'derek@corp.com', '2020-06-20', 2, 1); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Fay', 'Foster', 'Sales Manager', '555-2001', 'fay@corp.com', '2018-08-01', 3, 2); + +INSERT INTO EMPLOYEE (FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL, HIRE_DATE, MANAGER_EMP_NUM, DPT_NUM) +VALUES ('Gus', 'Green', 'Account Exec', '555-2002', 'gus@corp.com', '2021-01-15', 4, 2); + +-- Update departments with managers (using auto-generated IDs) +UPDATE DEPARTMENT SET MANAGER_EMP_NUM = 1 WHERE DPT_NAME = 'Engineering'; +UPDATE DEPARTMENT SET MANAGER_EMP_NUM = 2 WHERE DPT_NAME = 'Sales'; +UPDATE DEPARTMENT SET MANAGER_EMP_NUM = 3 WHERE DPT_NAME = 'Recruiting'; \ No newline at end of file diff --git a/src/main/resources/schema-h2.sql b/src/main/resources/schema-h2.sql deleted file mode 100644 index 030913b..0000000 --- a/src/main/resources/schema-h2.sql +++ /dev/null @@ -1,33 +0,0 @@ -DROP TABLE PERSON; - -CREATE TABLE PERSON ( - ID NUMBER(10,0) NOT NULL AUTO_INCREMENT, - FIRST_NAME VARCHAR2(255) NOT NULL DEFAULT '', - LAST_NAME VARCHAR2(255) NOT NULL DEFAULT '', - MOBILE VARCHAR2(20) NOT NULL DEFAULT '', - BIRTHDAY DATE DEFAULT NULL, - PRIMARY KEY (ID)); - -DROP TABLE HOME; - -CREATE TABLE HOME ( - ID NUMBER(10,0) NOT NULL AUTO_INCREMENT, - ADDRESS VARCHAR2(255) not null default '', - HOMENUMBER varchar2(255) NOT NULL DEFAULT '', - PRIMARY KEY (ID) -); - - -DROP TABLE CAR; - -CREATE TABLE CAR ( - ID NUMBER(10,0) NOT NULL AUTO_INCREMENT, - MAKE VARCHAR2(255) not null default '', - MODEL varchar2(255) NOT NULL DEFAULT '', - YEAR VARCHAR2(5) NOT NULL DEFAULT '01907', - PRIMARY KEY (ID) -); - -DROP SEQUENCE hibernate_sequence; - -CREATE SEQUENCE hibernate_sequence; \ No newline at end of file diff --git a/src/test/java/io/zipcoder/PersistenceStarterApplicationTests.java b/src/test/java/io/zipcoder/PersistenceStarterApplicationTests.java index 3e5dd20..a8cf87f 100644 --- a/src/test/java/io/zipcoder/PersistenceStarterApplicationTests.java +++ b/src/test/java/io/zipcoder/PersistenceStarterApplicationTests.java @@ -1,11 +1,12 @@ package io.zipcoder; -import org.junit.Test; -import org.junit.runner.RunWith; + +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; -@RunWith(SpringRunner.class) + +// @RunWith(SpringRunner.class) @SpringBootTest public class PersistenceStarterApplicationTests {