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 {