diff --git a/README.md b/README.md
index be35613..88763d9 100644
--- a/README.md
+++ b/README.md
@@ -49,3 +49,6 @@ A map of key/value pairs of properties assigned to an employee.
1. Create a Fork of the repository into your personal GitHub space.
2. Implement the feature as described above.
3. Create a Pull Request back to the original project.
+
+## Swagger docs
+The swagger documentation is in [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
diff --git a/pom.xml b/pom.xml
index 17fd7ad..00c67ea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,6 +23,10 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
org.springframework.boot
spring-boot-starter-data-jdbc
@@ -31,6 +35,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-devtools
+
org.flywaydb
@@ -49,6 +57,28 @@
spring-boot-starter-test
test
+
+
+ junit
+ junit
+
+
+
+ org.mockito
+ mockito-core
+
+
+
+ io.springfox
+ springfox-swagger2
+ 2.9.2
+
+
+
+ io.springfox
+ springfox-swagger-ui
+ 2.9.2
+
diff --git a/src/main/java/com/zoomcare/candidatechallenge/advice/EmployeeNotFoundAdvice.java b/src/main/java/com/zoomcare/candidatechallenge/advice/EmployeeNotFoundAdvice.java
new file mode 100644
index 0000000..00017de
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/advice/EmployeeNotFoundAdvice.java
@@ -0,0 +1,19 @@
+package com.zoomcare.candidatechallenge.advice;
+
+import com.zoomcare.candidatechallenge.exception.EmployeeNotFoundException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ControllerAdvice
+public class EmployeeNotFoundAdvice {
+
+ @ResponseBody
+ @ExceptionHandler(EmployeeNotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ String employeeNotFoundHandler(EmployeeNotFoundException ex){
+ return ex.getMessage();
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/config/SwaggerConfig.java b/src/main/java/com/zoomcare/candidatechallenge/config/SwaggerConfig.java
new file mode 100644
index 0000000..33f3b23
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/config/SwaggerConfig.java
@@ -0,0 +1,38 @@
+package com.zoomcare.candidatechallenge.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.Collections;
+
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+
+ @Bean
+ public Docket apiDocket() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ .select()
+ .apis(RequestHandlerSelectors.basePackage("com.zoomcare.candidatechallenge.controller"))
+ .paths(PathSelectors.any())
+ .build()
+ .apiInfo(getApiInfo());
+ }
+
+ private ApiInfo getApiInfo() {
+ return new ApiInfo("ZOOM CARE Code Challenge",
+ "Organization API Description",
+ "1.0",
+ null,
+ null,
+ "LICENSE",
+ "",
+ Collections.emptyList());
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/controller/OrganizationController.java b/src/main/java/com/zoomcare/candidatechallenge/controller/OrganizationController.java
new file mode 100644
index 0000000..4c0d181
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/controller/OrganizationController.java
@@ -0,0 +1,45 @@
+package com.zoomcare.candidatechallenge.controller;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.exception.EmployeeNotFoundException;
+import com.zoomcare.candidatechallenge.service.OrganizationService;
+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;
+
+import java.util.List;
+
+/**
+ * Organization Endpoint
+ */
+@RestController
+@RequestMapping("/api/v1/organization")
+public class OrganizationController {
+
+ private final OrganizationService organizationService;
+
+ public OrganizationController(OrganizationService organizationService) {
+ this.organizationService = organizationService;
+ }
+
+ /**
+ * Obtain the top-level employees
+ * @return
+ */
+ @GetMapping("top")
+ List getAllTopLevelsEmployees(){
+ return organizationService.getAllTopLevelsEmployees();
+ }
+
+ /**
+ * Obtain employee by ID
+ * @param id the employee Identifieer
+ * @return the employee information requested.
+ */
+ @GetMapping("/{id}")
+ EmployeeDTO getEmployeeById(@PathVariable("id") Long id){
+ return organizationService.findById(id)
+ .orElseThrow(()-> new EmployeeNotFoundException(id));
+ }
+}
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..1752ca5
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/dto/EmployeeDTO.java
@@ -0,0 +1,56 @@
+package com.zoomcare.candidatechallenge.dto;
+
+import java.util.List;
+import java.util.Map;
+
+public class EmployeeDTO {
+
+ private Long id;
+ private Long supervisorId;
+
+ private List properties;
+
+ private List employees;
+
+ public EmployeeDTO() {
+ }
+
+ public EmployeeDTO(Long id, Long supervisorId, List properties, List employees) {
+ this.id = id;
+ this.supervisorId = supervisorId;
+ this.properties = properties;
+ this.employees = employees;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getSupervisorId() {
+ return supervisorId;
+ }
+
+ public void setSupervisorId(Long supervisorId) {
+ this.supervisorId = supervisorId;
+ }
+
+ public List getEmployees() {
+ return employees;
+ }
+
+ public void setEmployees(List employees) {
+ this.employees = employees;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public void setProperties(List properties) {
+ this.properties = properties;
+ }
+}
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..ac61dd7
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/dto/PropertyDTO.java
@@ -0,0 +1,29 @@
+package com.zoomcare.candidatechallenge.dto;
+
+import java.io.Serializable;
+
+public class PropertyDTO implements Serializable {
+ private String key;
+ private String value;
+
+ public PropertyDTO(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/exception/EmployeeNotFoundException.java b/src/main/java/com/zoomcare/candidatechallenge/exception/EmployeeNotFoundException.java
new file mode 100644
index 0000000..82e8f36
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/exception/EmployeeNotFoundException.java
@@ -0,0 +1,7 @@
+package com.zoomcare.candidatechallenge.exception;
+
+public class EmployeeNotFoundException extends RuntimeException {
+ public EmployeeNotFoundException(Long id) {
+ super("Could not find employee " + id);
+ }
+}
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..9232644
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/Employee.java
@@ -0,0 +1,58 @@
+package com.zoomcare.candidatechallenge.model;
+
+import javax.persistence.*;
+import java.util.List;
+
+/**
+ * Model for Employee table
+ */
+@Entity
+public class Employee {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Column(name = "supervisor_id")
+ private Long supervisorId;
+
+ @OneToMany
+ @JoinColumn(name = "supervisor_id")
+ private List employees;
+
+ @OneToMany
+ @JoinColumn(name = "employee_id")
+ private List properties;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getSupervisorId() {
+ return supervisorId;
+ }
+
+ public void setSupervisorId(Long supervisorId) {
+ this.supervisorId = supervisorId;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public void setProperties(List properties) {
+ this.properties = properties;
+ }
+
+ public List getEmployees() {
+ return employees;
+ }
+
+ public void setEmployees(List employees) {
+ this.employees = employees;
+ }
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/model/EntityPropertyPk.java b/src/main/java/com/zoomcare/candidatechallenge/model/EntityPropertyPk.java
new file mode 100644
index 0000000..14b4948
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/EntityPropertyPk.java
@@ -0,0 +1,33 @@
+package com.zoomcare.candidatechallenge.model;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import java.io.Serializable;
+
+@Embeddable
+public class EntityPropertyPk implements Serializable {
+ @Column(name = "key")
+ private String key;
+
+ @ManyToOne
+ @JoinColumn(name = "employee_id")
+ private Employee employee;
+
+ public Employee getEmployee() {
+ return employee;
+ }
+
+ public void setEmployee(Employee employee) {
+ this.employee = employee;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+}
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..344225a
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/model/Property.java
@@ -0,0 +1,34 @@
+package com.zoomcare.candidatechallenge.model;
+
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import java.io.Serializable;
+
+/**
+ * Model for Property table
+ */
+@Entity
+public class Property implements Serializable {
+
+ @EmbeddedId
+ private EntityPropertyPk id;
+ private String value;
+
+ public EntityPropertyPk getId() {
+ return id;
+ }
+
+ public void setId(EntityPropertyPk 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/repository/EmployeeRepository.java b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java
new file mode 100644
index 0000000..47eae0c
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/repository/EmployeeRepository.java
@@ -0,0 +1,17 @@
+package com.zoomcare.candidatechallenge.repository;
+
+import com.zoomcare.candidatechallenge.model.Employee;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Data Access Layer for the Employee information.
+ */
+@Repository
+public interface EmployeeRepository extends JpaRepository {
+ List findAllBySupervisorIdIsNull();
+ Optional findById(Long id);
+}
diff --git a/src/main/java/com/zoomcare/candidatechallenge/service/OrganizationService.java b/src/main/java/com/zoomcare/candidatechallenge/service/OrganizationService.java
new file mode 100644
index 0000000..ff7b5a2
--- /dev/null
+++ b/src/main/java/com/zoomcare/candidatechallenge/service/OrganizationService.java
@@ -0,0 +1,57 @@
+package com.zoomcare.candidatechallenge.service;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.dto.PropertyDTO;
+import com.zoomcare.candidatechallenge.model.Employee;
+import com.zoomcare.candidatechallenge.model.Property;
+import com.zoomcare.candidatechallenge.repository.EmployeeRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Handles organization operations
+ */
+@Service
+public class OrganizationService {
+
+ private EmployeeRepository employeeRepository;
+
+ public OrganizationService(EmployeeRepository employeeRepository) {
+ this.employeeRepository = employeeRepository;
+ }
+
+ public Optional findById(Long id) {
+ return employeeRepository.findById(id).map(this::employeeToEmployeeDTO);
+ }
+
+ public List getAllTopLevelsEmployees(){
+ return employeeRepository.findAllBySupervisorIdIsNull().stream()
+ .map(this::employeeToEmployeeDTO)
+ .collect(Collectors.toList());
+ }
+
+ private EmployeeDTO employeeToEmployeeDTO(Employee employee){
+ EmployeeDTO dto = new EmployeeDTO();
+ dto.setId(employee.getId());
+ dto.setSupervisorId(employee.getSupervisorId());
+ if(!CollectionUtils.isEmpty(employee.getEmployees())){
+ dto.setEmployees(employee.getEmployees().stream().map(this::employeeToEmployeeDTO).collect(Collectors.toList()));
+ } else {
+ dto.setEmployees(Collections.emptyList());
+ }
+ if(!CollectionUtils.isEmpty(employee.getProperties())){
+ dto.setProperties(employee.getProperties().stream().map(this::mapProperty).collect(Collectors.toList()));
+ } else {
+ dto.setProperties(Collections.emptyList());
+ }
+ return dto;
+ }
+ private PropertyDTO mapProperty(Property property){
+ return new PropertyDTO(property.getId().getKey(), property.getValue());
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 4408d17..eff42ca 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,6 +2,8 @@ spring:
h2:
console:
enabled: true
+ main:
+ allow-bean-definition-overriding: true
management:
endpoints:
web:
diff --git a/src/test/java/com/zoomcare/candidatechallenge/controller/OrganizationControllerTest.java b/src/test/java/com/zoomcare/candidatechallenge/controller/OrganizationControllerTest.java
new file mode 100644
index 0000000..188af7f
--- /dev/null
+++ b/src/test/java/com/zoomcare/candidatechallenge/controller/OrganizationControllerTest.java
@@ -0,0 +1,40 @@
+package com.zoomcare.candidatechallenge.controller;
+
+import com.zoomcare.candidatechallenge.service.OrganizationService;
+import junit.framework.TestCase;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringRunner.class)
+@WebMvcTest(OrganizationController.class)
+public class OrganizationControllerTest extends TestCase {
+
+ @MockBean
+ private OrganizationService organizationService;
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Test
+ public void testGetAllTopLevelsEmployees() throws Exception{
+ mvc.perform(MockMvcRequestBuilders.get("/api/v1/organization/top"))
+ .andDo(print())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testGetEmployeeById() throws Exception{
+ mvc.perform(MockMvcRequestBuilders.get("/api/v1/organization/2"))
+ .andDo(print())
+ .andExpect(status().is4xxClientError());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/zoomcare/candidatechallenge/service/OrganizationServiceTest.java b/src/test/java/com/zoomcare/candidatechallenge/service/OrganizationServiceTest.java
new file mode 100644
index 0000000..934bfa5
--- /dev/null
+++ b/src/test/java/com/zoomcare/candidatechallenge/service/OrganizationServiceTest.java
@@ -0,0 +1,95 @@
+package com.zoomcare.candidatechallenge.service;
+
+import com.zoomcare.candidatechallenge.dto.EmployeeDTO;
+import com.zoomcare.candidatechallenge.model.Employee;
+import com.zoomcare.candidatechallenge.model.EntityPropertyPk;
+import com.zoomcare.candidatechallenge.model.Property;
+import com.zoomcare.candidatechallenge.repository.EmployeeRepository;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.when;
+
+public class OrganizationServiceTest {
+
+ @Mock
+ private EmployeeRepository employeeRepository;
+
+ private OrganizationService organizationService;
+
+ @Before
+ public void setUp(){
+ MockitoAnnotations.initMocks(this);
+ organizationService = new OrganizationService(employeeRepository);
+ }
+
+ @Test
+ public void testFindById(){
+ Long employee_id = 99L;
+ when(employeeRepository.findById(anyLong())).thenReturn(Optional.of(generateEmployee(employee_id)));
+
+ Optional result = organizationService.findById(employee_id);
+ assertTrue(result.isPresent());
+ assertFalse(result.get().getProperties().isEmpty());
+ assertEquals(2,result.get().getProperties().size());
+ assertEquals("KEY", result.get().getProperties().get(0).getKey());
+ assertEquals("KEY2", result.get().getProperties().get(1).getKey());
+ assertFalse(result.get().getEmployees().isEmpty());
+ assertEquals(1, result.get().getEmployees().size());
+ }
+
+ @Test
+ public void testGetAllTopLevelsEmployees(){
+ List employees = Arrays.asList(
+ generateEmployee(55L),
+ generateEmployee(66L),
+ generateEmployee(77L));
+ when(employeeRepository.findAllBySupervisorIdIsNull()).thenReturn(employees);
+
+ List employeeDTOs= organizationService.getAllTopLevelsEmployees();
+
+ assertFalse(employeeDTOs.isEmpty());
+ assertEquals(3, employeeDTOs.size() );
+ assertEquals(55L, employeeDTOs.get(0).getId().longValue() );
+ assertEquals(66L, employeeDTOs.get(1).getId().longValue() );
+ assertEquals(77L, employeeDTOs.get(2).getId().longValue() );
+ }
+
+ private Employee generateEmployee(Long employee_id){
+ Employee employee = new Employee();
+ employee.setId(employee_id);
+ List properties = new ArrayList<>();
+ Property property = new Property();
+ EntityPropertyPk pk = new EntityPropertyPk();
+ pk.setKey("KEY");
+ pk.setEmployee(employee);
+ property.setId(pk);
+ property.setValue("VALUE");
+ properties.add(property);
+
+ Property property2 = new Property();
+ EntityPropertyPk pk2 = new EntityPropertyPk();
+ pk2.setKey("KEY2");
+ pk2.setEmployee(employee);
+ property2.setId(pk2);
+ property2.setValue("VALUE2");
+ properties.add(property2);
+
+ employee.setProperties(properties);
+
+ List employees = new ArrayList<>();
+ employees.add(new Employee());
+ employee.setEmployees(employees);
+
+ return employee;
+ }
+}
\ No newline at end of file