diff --git a/pom.xml b/pom.xml index 17fd7ad..351e267 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + org.flywaydb @@ -49,6 +53,25 @@ spring-boot-starter-test test + + + org.modelmapper + modelmapper + 3.1.1 + + + + org.projectlombok + lombok + 1.18.28 + provided + + + + org.springframework.boot + spring-boot-devtools + + diff --git a/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java b/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java new file mode 100644 index 0000000..5369d8b --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/config/CandidateChallengeConfig.java @@ -0,0 +1,20 @@ +package com.zoomcare.candidatechallenge.config; + +import com.zoomcare.candidatechallenge.repository.EmployeeRepository; +import com.zoomcare.candidatechallenge.service.EmployeeService; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CandidateChallengeConfig { + @Bean + public ModelMapper modelMapper() { + return new ModelMapper(); + } + + @Bean + public EmployeeService employeeService(ModelMapper modelMapper, EmployeeRepository employeeRepository) { + return new EmployeeService(employeeRepository, modelMapper); + } +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java b/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java new file mode 100644 index 0000000..04b26bc --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/controller/EmployeeController.java @@ -0,0 +1,37 @@ +package com.zoomcare.candidatechallenge.controller; + +import com.zoomcare.candidatechallenge.dto.EmployeeDTO; +import com.zoomcare.candidatechallenge.service.EmployeeService; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/employees") +public class EmployeeController { + @Autowired + private EmployeeService employeeService; + + @GetMapping("/{id}") + ResponseEntity getById(@PathVariable Long id) { + Optional employeeDTO = employeeService.findById(id); + if (!employeeDTO.isPresent()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(employeeDTO.get()); + } + + @GetMapping("/topLevel") + ResponseEntity> getTopLevelEmployees() { + List employeeDTOs = employeeService.getTopLevelEmployee(); + if (employeeDTOs.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(employeeDTOs); + } +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java new file mode 100644 index 0000000..3eeaf1a --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java @@ -0,0 +1,18 @@ +package com.zoomcare.candidatechallenge.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class EmployeeDTO { + private Long id; + private Long supervisorId; + private List properties; + private List directReports; +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java new file mode 100644 index 0000000..7dfbc00 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java @@ -0,0 +1,11 @@ +package com.zoomcare.candidatechallenge.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class PropertyDTO { + private PropertyPKDTO id; + private String value; +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java new file mode 100644 index 0000000..fcda728 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyPKDTO.java @@ -0,0 +1,10 @@ +package com.zoomcare.candidatechallenge.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class PropertyPKDTO { + private String key; +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java b/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java new file mode 100644 index 0000000..efa3a91 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java @@ -0,0 +1,59 @@ +package com.zoomcare.candidatechallenge.model; + +import java.io.Serializable; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import lombok.Builder; + +@Builder +@Entity +public class Employee implements Serializable { + @Id + @GeneratedValue + private Long id; + + @Column(name = "supervisor_id") + private Long supervisorId; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "employee_id") + private List properties; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "supervisor_id") + private List directReports; + + public Long getSupervisorId() { + return supervisorId; + } + + public void setSupervisorId(Long supervisorId) { + this.supervisorId = supervisorId; + } + + public List getDirectReports() { + return directReports; + } + + public void setDirectReports(List directReports) { + this.directReports = directReports; + } + + public Long getId() { + return id; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/Property.java b/src/main/java/com/zoomcare/candidatechallenge/model/Property.java new file mode 100644 index 0000000..d93a408 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/model/Property.java @@ -0,0 +1,30 @@ +package com.zoomcare.candidatechallenge.model; + +import java.io.Serializable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import lombok.Builder; + +@Entity +@Builder +public class Property implements Serializable { + @EmbeddedId + private PropertyPK id; + private String value; + + public PropertyPK getId() { + return id; + } + + public void setId(PropertyPK id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java b/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java new file mode 100644 index 0000000..db193e8 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/model/PropertyPK.java @@ -0,0 +1,32 @@ +package com.zoomcare.candidatechallenge.model; + +import java.io.Serializable; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.Builder; + +@Embeddable +@Builder +public class PropertyPK implements Serializable { + + private String key; + + @Column(name = "employee_id") + private Long employeeId; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java new file mode 100644 index 0000000..8694f0a --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java @@ -0,0 +1,11 @@ +package com.zoomcare.candidatechallenge.repository; + +import com.zoomcare.candidatechallenge.model.Employee; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EmployeeRepository extends JpaRepository { + Optional findById(Long id); + List findAllBySupervisorIdIsNull(); +} diff --git a/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java b/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java new file mode 100644 index 0000000..a2c1818 --- /dev/null +++ b/src/main/java/com/zoomcare/candidatechallenge/service/EmployeeService.java @@ -0,0 +1,36 @@ +package com.zoomcare.candidatechallenge.service; + +import com.zoomcare.candidatechallenge.dto.EmployeeDTO; +import com.zoomcare.candidatechallenge.model.Employee; +import com.zoomcare.candidatechallenge.repository.EmployeeRepository; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Service; + +@Service +public class EmployeeService { + private final EmployeeRepository employeeRepository; + private final ModelMapper modelMapper; + + public EmployeeService(EmployeeRepository employeeRepository, ModelMapper modelMapper) { + this.employeeRepository = employeeRepository; + this.modelMapper = modelMapper; + } + + public Optional findById(Long id) { + return employeeRepository.findById(id).map(this::convertToDto); + } + + public List getTopLevelEmployee() { + return employeeRepository.findAllBySupervisorIdIsNull().stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + public EmployeeDTO convertToDto(Employee employee) { + return modelMapper.map(employee, EmployeeDTO.class); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4408d17..69ee7c7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,6 @@ spring: + main: + allow-bean-definition-overriding: true h2: console: enabled: true diff --git a/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java b/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java new file mode 100644 index 0000000..b31af1c --- /dev/null +++ b/src/test/java/com/zoomcare/candidatechallenge/service/EmployeeServiceTest.java @@ -0,0 +1,110 @@ +package com.zoomcare.candidatechallenge.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import com.zoomcare.candidatechallenge.dto.EmployeeDTO; +import com.zoomcare.candidatechallenge.model.Employee; +import com.zoomcare.candidatechallenge.model.Property; +import com.zoomcare.candidatechallenge.model.PropertyPK; +import com.zoomcare.candidatechallenge.repository.EmployeeRepository; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.modelmapper.ModelMapper; + +@RunWith(MockitoJUnitRunner.class) +public class EmployeeServiceTest { + private final Long EMPLOYEE_ID = 2L; + private final Long SUPERVISOR_ID = 1L; + private final String PROPERTY_KEY = "key"; + private Employee employee; + private Employee supervisorEmployee; + + @Mock + private EmployeeRepository employeeRepository; + + private PropertyPK propertyPK; + private Property property; + private ModelMapper modelMapper; + + private EmployeeService employeeService; + + @Before + public void setUp() { + modelMapper = new ModelMapper(); + employeeService = new EmployeeService(employeeRepository, modelMapper); + + propertyPK = PropertyPK.builder() + .employeeId(EMPLOYEE_ID) + .key(PROPERTY_KEY) + .build(); + + property = Property.builder() + .id(propertyPK) + .value("value") + .build(); + + employee = Employee.builder() + .id(EMPLOYEE_ID) + .supervisorId(SUPERVISOR_ID) + .properties(Arrays.asList(property)) + .build(); + + supervisorEmployee = Employee.builder() + .id(1l) + .directReports(Arrays.asList(employee)) + .build(); + + } + + @Test + public void findByIdFindsEmployeeTest() { + when(employeeRepository.findById(EMPLOYEE_ID)).thenReturn(Optional.of(employee)); + + Optional result = employeeService.findById(EMPLOYEE_ID); + + assertTrue(result.isPresent()); + EmployeeDTO employeeDTO = result.get(); + assertEquals(EMPLOYEE_ID, employeeDTO.getId()); + assertEquals(propertyPK.getKey(), employeeDTO.getProperties().get(0).getId().getKey()); + assertEquals(property.getValue(), employeeDTO.getProperties().get(0).getValue()); + assertEquals(employee.getSupervisorId(), employeeDTO.getSupervisorId()); + assertEquals(employee.getProperties().get(0).getValue(), employeeDTO.getProperties().get(0).getValue()); + } + + @Test + public void findByIdEmployeeNotFoundTest() { + Optional result = employeeService.findById(3L); + + assertFalse(result.isPresent()); + } + + @Test + public void getTopLevelEmployeeFoundTest() { + when(employeeRepository.findAllBySupervisorIdIsNull()).thenReturn(Arrays.asList(supervisorEmployee)); + + List supervisors = employeeService.getTopLevelEmployee(); + + assertFalse(supervisors.isEmpty()); + EmployeeDTO supervisor = supervisors.get(0); + assertEquals(SUPERVISOR_ID, supervisor.getId()); + assertEquals(supervisorEmployee.getSupervisorId(), supervisor.getSupervisorId()); + assertEquals(supervisorEmployee.getDirectReports().size(), supervisor.getDirectReports().size()); + assertEquals(EMPLOYEE_ID, supervisor.getDirectReports().get(0).getId()); + } + + @Test + public void getTopLevelEmployeeNotFoundTest() { + List supervisors = employeeService.getTopLevelEmployee(); + + assertTrue(supervisors.isEmpty()); + } +} \ No newline at end of file