I’m writing implementation of Employee Catalog using Java Spring Boot.
Here are the details of instructions:
Create a webapp serving somewhat like a REST catalog of employees stored in embedded database.
Implement endpoints serving employees as JSON representations of springemployeecatalog.domain.* classes.
Each employee record should include information of his department and his manager. Manager’s manager should be usually null.
Endpoints (all serves GET requests):
/employees – list all employees. Supports paging*.
/employees/{employee_id} – single employee. If parameter named full_chain exists and is set to true then full manager chain should be written (include employee`s manager, manager of manager, manager of manager of manager and so on up to the organization head)
/employees/by_manager/{managerId} – list employees who subordinates to the manager. No transitivity. Supports paging*.
/employees/by_department/{departmentId or departmentName} – list employees who is working in the department. Supports paging*.
- Supports paging – means that you may manage what sublist of employees you want to get by three parameters:
page – number of the page (starts with 0)
size – amount of entry per page
sort – name of the field for sorting (single value from list [lastName, hired, position, salary], order is ascending)
Reminder:
You may not change domain classes
You may not change sql scripts
You should serve employees from the database without any in-memory storage
Here’s the code implementation I wrote and domain classes:
public class Department {
private final Long id;
private final String name;
private final String location;
@JsonCreator
public Department(@JsonProperty("id") final Long id,
@JsonProperty("name") final String name,
@JsonProperty("location") final String location) {
this.id = id;
this.name = name;
this.location = location;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getLocation() {
return location;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Department that = (Department) o;
return Objects.equal(id, that.id) &&
Objects.equal(name, that.name) &&
Objects.equal(location, that.location);
}
@Override
public int hashCode() {
return Objects.hashCode(id, name, location);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("location", location)
.toString();
}
public static class Parser {
private static ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false);
}
public static String toJson(Department department){
try {
final StringWriter writer = new StringWriter();
mapper.writeValue(writer, department);
return writer.toString();
} catch (IOException exc) {
throw new RuntimeException(exc);
}
}
public static Department parseJson(String json){
try {
return mapper.readValue(json, Department.class);
} catch (IOException exc) {
throw new RuntimeException(exc);
}
}
}
}
public class Employee {
private final Long id;
private final FullName fullName;
private final Position position;
private final LocalDate hired;
private final BigDecimal salary;
private final Employee manager;
private final Department department;
@JsonCreator
public Employee(@JsonProperty("id") final Long id,
@JsonProperty("fullName") final FullName fullName,
@JsonProperty("position") final Position position,
@JsonProperty("hired") final LocalDate hired,
@JsonProperty("salary") final BigDecimal salary,
@JsonProperty("manager") final Employee manager,
@JsonProperty("department") final Department department) {
this.id = id;
this.fullName = fullName;
this.position = position;
this.hired = hired;
this.salary = salary.setScale(5, RoundingMode.HALF_UP);
this.manager = manager;
this.department = department;
}
public Long getId() {
return id;
}
public FullName getFullName() {
return fullName;
}
public Position getPosition() {
return position;
}
public LocalDate getHired() {
return hired;
}
public BigDecimal getSalary() {
return salary;
}
public Employee getManager() {
return manager;
}
public Department getDepartment() {
return department;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Employee employee = (Employee) o;
return Objects.equal(id, employee.id) &&
Objects.equal(fullName, employee.fullName) &&
position == employee.position &&
Objects.equal(hired, employee.hired) &&
Objects.equal(salary, employee.salary) &&
Objects.equal(manager, employee.manager) &&
Objects.equal(department, employee.department);
}
@Override
public int hashCode() {
return Objects.hashCode(id, fullName, position, hired, salary, manager, department);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("fullName", fullName)
.add("position", position)
.add("hired", hired)
.add("salary", salary)
.add("manager", manager)
.add("department", department)
.toString();
}
public static class Parser {
private static ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false);
}
public static String toJson(Employee employee) {
try {
final StringWriter writer = new StringWriter();
mapper.writeValue(writer, employee);
return writer.toString();
} catch (IOException exc) {
throw new RuntimeException(exc);
}
}
public static Employee parseJson(String json) {
try {
return mapper.readValue(json, Employee.class);
} catch (IOException exc) {
throw new RuntimeException(exc);
}
}
}
}
public class FullName {
private final String firstName;
private final String lastName;
private final String middleName;
@JsonCreator
public FullName(@JsonProperty("firstName") final String firstName,
@JsonProperty("lastName") final String lastName,
@JsonProperty("middleName") final String middleName) {
this.firstName = firstName;
this.lastName = lastName;
this.middleName = middleName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getMiddleName() {
return middleName;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final FullName fullName = (FullName) o;
return Objects.equal(firstName, fullName.firstName) &&
Objects.equal(lastName, fullName.lastName) &&
Objects.equal(middleName, fullName.middleName);
}
@Override
public int hashCode() {
return Objects.hashCode(firstName, lastName, middleName);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("firstName", firstName)
.add("lastName", lastName)
.add("middleName", middleName)
.toString();
}
}
public enum Position {
PRESIDENT,
MANAGER,
ANALYST,
CLERK,
SALESMAN
}
@RestController
@RequestMapping("/employees")
public class EmployeeController {
private final EmployeeService employeeService;
@Autowired
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@GetMapping
public List<Employee> getAllEmployees(@RequestParam(defaultValue = "0") int page,
@RequestParam int size,
@RequestParam String sort) {
return employeeService.getAllEmployees(page, size, sort);
}
@GetMapping("/{employee_id}")
public Optional<Employee> getEmployeeById(@PathVariable("employee_id") Long employeeId,
@RequestParam(defaultValue = "false") boolean full_chain) {
return employeeService.getEmployeeById(employeeId, full_chain);
}
@GetMapping("/by_manager/{managerId}")
public List<Employee> getEmployeeByManager(@PathVariable("managerId") Long managerId,
@RequestParam(defaultValue = "0") int page,
@RequestParam int size,
@RequestParam String sort) {
return employeeService.getEmployeeByManager(managerId, page, size, sort);
}
@GetMapping("/by_department/{departmentId}")
public List<Employee> getEmployeeByDepartment(@PathVariable("departmentId") Long departmentId,
@RequestParam(defaultValue = "0") int page,
@RequestParam int size,
@RequestParam String sort) {
return employeeService.getEmployeeByDepartment(departmentId, page, size, sort);
}
}
@Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
@Autowired
public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
public List<Employee> getAllEmployees(int page, int size, String sort) {
return employeeRepository.findAll(page, size, sort);
}
public Optional<Employee> getEmployeeById(Long employeeId, boolean fullChain) {
return employeeRepository.findById(employeeId, fullChain);
}
public List<Employee> getEmployeeByManager(Long managerId, int page, int size, String sort) {
return employeeRepository.findEmployeesByManager(managerId, page, size, sort);
}
public List<Employee> getEmployeeByDepartment(Long departmentId, int page, int size, String sort) {
return employeeRepository.findEmployeesByDepartmentId(departmentId, page, size, sort);
}
}
@SpringBootApplication
public class MvcApplication {
public static void main(String[] args) {
SpringApplication.run(MvcApplication.class, args);
}
}
@Repository
public interface EmployeeRepository{
List<Employee> findAll(int page, int size, String sort);
Optional<Employee> findById(Long id, boolean fullChain);
List<Employee> findEmployeesByManager(Long managerId, int page, int size, String sort);
List<Employee> findEmployeesByDepartmentId(Long departmentId, int page, int size, String sort);
}
@Repository
public class EmployeeRepositoryImpl implements EmployeeRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public EmployeeRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public List<Employee> findAll(int page, int size, String sort) {
String query = "SELECT * FROM EMPLOYEE ORDER BY " + sort;
List<Employee> employees = jdbcTemplate.query(query, new EmployeeRowMapper(jdbcTemplate));
return getList(employees, page, size);
}
@Override
public Optional<Employee> findById(Long id, boolean fullChain) {
String query = "SELECT * FROM EMPLOYEE WHERE ID = ?";
Employee employee = jdbcTemplate.queryForObject(query, new Object[]{id}, new EmployeeRowMapper(jdbcTemplate));
return Optional.ofNullable(employee);
}
@Override
public List<Employee> findEmployeesByManager(Long managerId, int page, int size, String sort) {
String query = "SELECT * FROM EMPLOYEE WHERE MANAGER = ? ORDER BY " + sort;
List<Employee> employees = jdbcTemplate.query(query, new Object[]{managerId}, new EmployeeRowMapper(jdbcTemplate));
return getList(employees, page, size);
}
@Override
public List<Employee> findEmployeesByDepartmentId(Long departmentId, int page, int size, String sort) {
String query = "SELECT * FROM EMPLOYEE WHERE DEPARTMENT = ? ORDER BY " + sort;
List<Employee> employees = jdbcTemplate.query(query, new Object[]{departmentId}, new EmployeeRowMapper(jdbcTemplate));
return getList(employees, page, size);
}
private List<Employee> getList(List<Employee> employees, int page, int size) {
int startIndex = page * size;
int endIndex = Math.min(startIndex + size, employees.size());
return employees.subList(startIndex, endIndex);
}
private static class EmployeeRowMapper implements RowMapper<Employee> {
private final JdbcTemplate jdbcTemplate;
private EmployeeRowMapper(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
private static List<Long> employeeManager = new ArrayList<>();
@Override
public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
Long id = resultSet.getLong("ID");
String firstName = resultSet.getString("FIRSTNAME");
String lastName = resultSet.getString("LASTNAME");
String middleName = resultSet.getString("MIDDLENAME");
FullName fullName = new FullName(firstName, lastName, middleName);
Position position = Position.valueOf(resultSet.getString("POSITION"));
LocalDate hired = resultSet.getDate("HIREDATE").toLocalDate();
BigDecimal salary = BigDecimal.valueOf(resultSet.getDouble("SALARY"));
Long managerId = resultSet.getLong("MANAGER");
Employee manager = null;
if (employeeManager.isEmpty()) {
manager = findEmployee(managerId);
}
Long departmentId = resultSet.getLong("DEPARTMENT");
Department department = findDepartment(departmentId);
Employee employee = new Employee(id, fullName, position, hired, salary, manager, department);
return employee;
}
private Department findDepartment(Long departmentId) {
if (departmentId == 0) return null;
String query = "SELECT * FROM DEPARTMENT WHERE ID = ?";
return jdbcTemplate.queryForObject(query, new Object[]{departmentId}, new DepartmentRowMapper());
}
private Employee findEmployee(Long managerId) {
if (managerId == 0) return null;
employeeManager.add(managerId);
String query = "SELECT * FROM EMPLOYEE WHERE ID = ?";
return jdbcTemplate.queryForObject(query, new Object[]{managerId}, new EmployeeRowMapper(jdbcTemplate));
}
private static class DepartmentRowMapper implements RowMapper<Department> {
@Override
public Department mapRow(ResultSet resultSet, int i) throws SQLException {
Long id = resultSet.getLong("ID");
String name = resultSet.getString("NAME");
String location = resultSet.getString("LOCATION");
return new Department(id, name, location);
}
}
}
}
I want to split findEmployee method in RowMapper in two, so one finds full manager chain and one finds only manager of an employee, but doesn’t continue after that. (Employee – Manager of an employee – Manager of manager is set to null). I also have findById method, which takes fullChain value as parameter and I want to connect it to RowMapper somehow.
Right now, I make a list employeeManager which saves Id’s of managers and I check if list is empty and if it is not I leave manager to be null, so manager of manager will be null, but it doesn’t work like that, since list is saved for whole application.