diff --git a/.gitignore b/.gitignore index a1c2a23..a64734d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,22 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +logs/ +*.lst diff --git a/pom.xml b/pom.xml index 0a3999d..6ecc67a 100644 --- a/pom.xml +++ b/pom.xml @@ -3,6 +3,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.5 + + + com.mastery simplewebapp 0.0.1-SNAPSHOT @@ -11,6 +18,82 @@ demo Practical task + + 1.9 + 1.9 + 1.4.2.Final + 1.18.16 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.mapstruct + mapstruct + 1.4.2.Final + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.validator + hibernate-validator + 6.2.0.Final + + + com.h2database + h2 + 1.4.200 + + + org.projectlombok + lombok + ${lombok.version} + + + org.postgresql + postgresql + 42.2.20 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.9 + 1.9 + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + diff --git a/src/main/java/com/mastery/java/task/Application.java b/src/main/java/com/mastery/java/task/Application.java new file mode 100644 index 0000000..04b7089 --- /dev/null +++ b/src/main/java/com/mastery/java/task/Application.java @@ -0,0 +1,12 @@ +package com.mastery.java.task; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/mastery/java/task/config/AppConfiguration.java b/src/main/java/com/mastery/java/task/config/AppConfiguration.java deleted file mode 100644 index 159a276..0000000 --- a/src/main/java/com/mastery/java/task/config/AppConfiguration.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.mastery.java.task.config; - -public class AppConfiguration { -} diff --git a/src/main/java/com/mastery/java/task/dao/EmployeeDao.java b/src/main/java/com/mastery/java/task/dao/EmployeeDao.java index edac5b1..00668c4 100644 --- a/src/main/java/com/mastery/java/task/dao/EmployeeDao.java +++ b/src/main/java/com/mastery/java/task/dao/EmployeeDao.java @@ -1,4 +1,19 @@ package com.mastery.java.task.dao; -public class EmployeeDao { +import com.mastery.java.task.model.Employee; + +import java.util.List; +import java.util.Optional; + +public interface EmployeeDao { + + Employee create(Employee employee); + + List findAll(); + + Optional findById(long id); + + Employee update(long id, Employee employee); + + void delete(long id); } diff --git a/src/main/java/com/mastery/java/task/dao/impl/EmployeeDaoImpl.java b/src/main/java/com/mastery/java/task/dao/impl/EmployeeDaoImpl.java new file mode 100644 index 0000000..ab1e262 --- /dev/null +++ b/src/main/java/com/mastery/java/task/dao/impl/EmployeeDaoImpl.java @@ -0,0 +1,79 @@ +package com.mastery.java.task.dao.impl; + +import com.mastery.java.task.dao.EmployeeDao; +import com.mastery.java.task.mapper.row_mapper.EmployeeRowMapper; +import com.mastery.java.task.model.Employee; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Repository +public class EmployeeDaoImpl implements EmployeeDao { + + private final JdbcTemplate jdbcTemplate; + private SimpleJdbcInsert jdbcInsert; + + @Autowired + public EmployeeDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withSchemaName("employeedb.public") + .withTableName("\"‘employee’ \"") + .usingColumns("first_name", "last_name", "department_id", + "job_title", "gender_id", "date_of_birth") + .usingGeneratedKeyColumns("employee_id"); + ; + } + + @Override + public Employee create(Employee employee) { + Number employeeId = jdbcInsert.executeAndReturnKey( + Map.of("first_name", employee.getFirstName(), + "last_name", employee.getLastName(), + "department_id", employee.getDepartmentId(), + "job_title", employee.getJobTitle(), + "gender_id", employee.getGender().getId(), + "date_of_birth", employee.getDateOfBirth()) + ); + + return employee.setId(employeeId.longValue()); + } + + @Override + public List findAll() { + return jdbcTemplate.query( + "select employee_id, first_name, last_name, department_id, job_title, gender_id, date_of_birth " + + "from public.\"‘employee’ \"", + new EmployeeRowMapper()); + } + + @Override + public Optional findById(long id) { + return jdbcTemplate.query + ("select employee_id, first_name, last_name, department_id, job_title, gender_id, date_of_birth " + + "from public.\"‘employee’ \" where employee_id = ?", + new Object[]{id}, new EmployeeRowMapper()) + .stream().findAny(); + } + + @Override + public Employee update(long id, Employee employee) { + jdbcTemplate.update( + "update public.\"‘employee’ \" set first_name = ?, last_name = ?, department_id = ?, job_title = ?, gender_id = ?, date_of_birth = ? " + + "where employee_id = ?", + employee.getFirstName(), employee.getLastName(), employee.getDepartmentId(), + employee.getJobTitle(), employee.getGender().getId(), employee.getDateOfBirth(), id); + + return employee.setId(id); + } + + @Override + public void delete(long id) { + jdbcTemplate.update("delete from public.\"‘employee’ \" where employee_id = ?", id); + } +} diff --git a/src/main/java/com/mastery/java/task/dto/Employee.java b/src/main/java/com/mastery/java/task/dto/Employee.java deleted file mode 100644 index d79d397..0000000 --- a/src/main/java/com/mastery/java/task/dto/Employee.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.mastery.java.task.dto; - -public class Employee { - private Long employeeId; - private String firstName; - private Gender gender; -} diff --git a/src/main/java/com/mastery/java/task/dto/EmployeeDto.java b/src/main/java/com/mastery/java/task/dto/EmployeeDto.java new file mode 100644 index 0000000..aae6895 --- /dev/null +++ b/src/main/java/com/mastery/java/task/dto/EmployeeDto.java @@ -0,0 +1,38 @@ +package com.mastery.java.task.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EmployeeDto { + + private Long employeeId; + + @NotEmpty(message = "First name cannot be empty") + private String firstName; + + @NotEmpty(message = "First name cannot be empty") + private String lastName; + + @Min(value = 0, message = "Department id has to be greater than 0") + @NotNull(message= "Department id cannot be empty") + private Long departmentId; + + @NotEmpty(message = "Job title cannot be empty") + private String jobTitle; + + @NotEmpty(message = "Gender cannot be empty") + private String gender; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime dateOfBirth; +} diff --git a/src/main/java/com/mastery/java/task/dto/Gender.java b/src/main/java/com/mastery/java/task/dto/Gender.java deleted file mode 100644 index 37cd1bf..0000000 --- a/src/main/java/com/mastery/java/task/dto/Gender.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.mastery.java.task.dto; - -public enum Gender { - MALE, - FEMALE -} diff --git a/src/main/java/com/mastery/java/task/error_handler/ApiError.java b/src/main/java/com/mastery/java/task/error_handler/ApiError.java new file mode 100644 index 0000000..37c46ca --- /dev/null +++ b/src/main/java/com/mastery/java/task/error_handler/ApiError.java @@ -0,0 +1,27 @@ +package com.mastery.java.task.error_handler; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpStatus; + +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +public class ApiError { + + private HttpStatus status; + private String error; + private List errors; + + public ApiError(HttpStatus status, + String error, + String errorAsList) { + this.status = status; + this.error = error; + this.errors = Collections.singletonList(errorAsList); + } +} \ No newline at end of file diff --git a/src/main/java/com/mastery/java/task/error_handler/RestResponseEntityExceptionHandler.java b/src/main/java/com/mastery/java/task/error_handler/RestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..cc2df66 --- /dev/null +++ b/src/main/java/com/mastery/java/task/error_handler/RestResponseEntityExceptionHandler.java @@ -0,0 +1,269 @@ +package com.mastery.java.task.error_handler; + +import com.mastery.java.task.exeption.EmployeeNotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Slf4j +@RestControllerAdvice +public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + + + @ExceptionHandler(EmployeeNotFoundException.class) + public ResponseEntity mapNEmployeeNotFoundException(EmployeeNotFoundException e) { + ApiError apiError = new ApiError( + HttpStatus.NOT_FOUND, + "Resource Not Found", + e.getMessage()); + + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(apiError); + } + + + @ExceptionHandler(DataAccessException.class) + public ResponseEntity mapDataAccessException(DataAccessException e) { + ApiError apiError = e.getClass().equals(DataIntegrityViolationException.class) ? + new ApiError( + HttpStatus.BAD_REQUEST, + "Constraints Violation", + "Cannot Insert Invalid Data") + : new ApiError( + HttpStatus.BAD_REQUEST, + "Data already exists", + e.getMessage()); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(apiError); + } + + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException e, WebRequest request) { + if (e.getMessage().contains("No enum constant com.mastery.java.task.model.Gender.")) { + ApiError apiError = new ApiError( + HttpStatus.BAD_REQUEST, + "Invalid gender", + "Gender has to be either MALE or FEMALE"); + + return ResponseEntity + .badRequest() + .body(apiError); + } + + return handleAll(e, request); + } + + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException(ConstraintViolationException e) { + List errors = new ArrayList<>(); + for (ConstraintViolation violation : e.getConstraintViolations()) { + errors.add(violation.getMessage() + ": " + violation.getInvalidValue()); + } + + ApiError apiError = new ApiError( + HttpStatus.BAD_REQUEST, + "Constraint Violations", + errors); + + return ResponseEntity + .badRequest() + .body(apiError); + } + + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, + WebRequest request) { + + List errors = new ArrayList<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.add(error.getField() + ": " + error.getDefaultMessage()); + } + for (ObjectError error : ex.getBindingResult().getGlobalErrors()) { + errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); + } + + ApiError apiError = new ApiError( + HttpStatus.BAD_REQUEST, + "Validation Errors", + errors); + + return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request); + } + + + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + ApiError apiError = new ApiError( + HttpStatus.BAD_REQUEST, + "Malformed JSON Request" , + "Passed JSON Cannot Be Read"); + + return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request); + } + + + @Override + protected ResponseEntity handleTypeMismatch( + TypeMismatchException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + ApiError apiError = new ApiError( + HttpStatus.BAD_REQUEST, + "Type Mismatch", + "Wrong " + ((MethodArgumentTypeMismatchException) ex).getName() + " parameter."); + + return handleExceptionInternal(ex, apiError, headers, apiError.getStatus(), request); + } + + + @Override + protected ResponseEntity handleHttpMediaTypeNotSupported( + HttpMediaTypeNotSupportedException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + StringBuilder builder = new StringBuilder(); + builder.append(ex.getContentType()). + append(" media type is not supported. Supported media types are "); + ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); + + int index = builder.lastIndexOf(", "); + if (index != -1) { + builder.delete(index, builder.length()); + } + + ApiError apiError = new ApiError( + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + "Unsupported Media Type" , + Collections.singletonList(builder.toString())); + + return ResponseEntity + .status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .body(apiError); + } + + + @Override + protected ResponseEntity handleHttpMediaTypeNotAcceptable( + HttpMediaTypeNotAcceptableException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + StringBuilder builder = new StringBuilder(); + builder.append(request.getHeader("accept")) + .append(" media type is not supported. Supported media types are: "); + ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); + + int index = builder.lastIndexOf(", "); + if (index != -1) { + builder.delete(index, builder.length()); + } + + ApiError apiError = new ApiError( + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + "Unsupported Media Type" , + Collections.singletonList(builder.toString())); + + return ResponseEntity + .status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .contentType(MediaType.APPLICATION_JSON) + .body(apiError); + } + + + @Override + protected ResponseEntity handleHttpRequestMethodNotSupported( + HttpRequestMethodNotSupportedException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + ApiError apiError = new ApiError( + HttpStatus.NOT_FOUND, + "Request Method Not Supported", + Collections.singletonList(String.format("Could not find the %s method", + ex.getMethod()))); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(apiError); + } + + + @Override + protected ResponseEntity handleNoHandlerFoundException( + NoHandlerFoundException ex, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + + ApiError apiError = new ApiError( + HttpStatus.NOT_FOUND, + "Method Not Found", + Collections.singletonList(String.format("Could not find the %s method for URL %s", + ex.getHttpMethod(), ex.getRequestURL()))); + + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(apiError); + } + + + @ExceptionHandler(Exception.class ) + public ResponseEntity handleAll( + Exception ex, + WebRequest request) { + + ApiError apiError = new ApiError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Internal Server Error" , + Collections.singletonList(ex.getMessage())); + + log.error("", ex); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(apiError); + } +} diff --git a/src/main/java/com/mastery/java/task/exeption/EmployeeNotFoundException.java b/src/main/java/com/mastery/java/task/exeption/EmployeeNotFoundException.java new file mode 100644 index 0000000..f6a3987 --- /dev/null +++ b/src/main/java/com/mastery/java/task/exeption/EmployeeNotFoundException.java @@ -0,0 +1,8 @@ +package com.mastery.java.task.exeption; + +public class EmployeeNotFoundException extends RuntimeException { + + public EmployeeNotFoundException(long id) { + super(String.format("No employee with id = %d found", id)); + } +} diff --git a/src/main/java/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapper.java b/src/main/java/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapper.java new file mode 100644 index 0000000..238b034 --- /dev/null +++ b/src/main/java/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapper.java @@ -0,0 +1,21 @@ +package com.mastery.java.task.mapper.entity_dto_mapper; + +import com.mastery.java.task.dto.EmployeeDto; +import com.mastery.java.task.model.Employee; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper(componentModel = "spring") +public interface EmployeeDtoMapper { + + EmployeeDto mapToDto(Employee employee); + Employee mapToEntity(EmployeeDto dto); + + @BeanMapping(nullValuePropertyMappingStrategy = + NullValuePropertyMappingStrategy.IGNORE) + @Mapping(target = "id", ignore = true) + void update(EmployeeDto dto, @MappingTarget Employee employee); +} diff --git a/src/main/java/com/mastery/java/task/mapper/row_mapper/EmployeeRowMapper.java b/src/main/java/com/mastery/java/task/mapper/row_mapper/EmployeeRowMapper.java new file mode 100644 index 0000000..602189b --- /dev/null +++ b/src/main/java/com/mastery/java/task/mapper/row_mapper/EmployeeRowMapper.java @@ -0,0 +1,23 @@ +package com.mastery.java.task.mapper.row_mapper; + +import com.mastery.java.task.model.Employee; +import com.mastery.java.task.model.Gender; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class EmployeeRowMapper implements RowMapper { + @Override + public Employee mapRow(ResultSet rs, int i) throws SQLException { + return new Employee( + rs.getLong("employee_id"), + rs.getString("first_name"), + rs.getString("last_name"), + rs.getInt("department_id"), + rs.getString("job_title"), + Gender.genderById(rs.getInt("gender_id")), + rs.getTimestamp("date_of_birth").toLocalDateTime() + ); + } +} diff --git a/src/main/java/com/mastery/java/task/model/Employee.java b/src/main/java/com/mastery/java/task/model/Employee.java new file mode 100644 index 0000000..612ee2f --- /dev/null +++ b/src/main/java/com/mastery/java/task/model/Employee.java @@ -0,0 +1,26 @@ +package com.mastery.java.task.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Employee { + + private long employeeId; + private String firstName; + private String lastName; + private long departmentId; + private String jobTitle; + private Gender gender; + private LocalDateTime dateOfBirth; + + public Employee setId(long employeeId) { + this.employeeId = employeeId; + return this; + } +} diff --git a/src/main/java/com/mastery/java/task/model/Gender.java b/src/main/java/com/mastery/java/task/model/Gender.java new file mode 100644 index 0000000..1821f74 --- /dev/null +++ b/src/main/java/com/mastery/java/task/model/Gender.java @@ -0,0 +1,20 @@ +package com.mastery.java.task.model; + +public enum Gender { + MALE(1), + FEMALE(2); + + private int id; + + Gender(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static Gender genderById(int id) { + return id == 1 ? MALE : FEMALE; + } +} diff --git a/src/main/java/com/mastery/java/task/rest/EmployeeController.java b/src/main/java/com/mastery/java/task/rest/EmployeeController.java index 3bc744c..142be2c 100644 --- a/src/main/java/com/mastery/java/task/rest/EmployeeController.java +++ b/src/main/java/com/mastery/java/task/rest/EmployeeController.java @@ -1,4 +1,84 @@ package com.mastery.java.task.rest; +import com.mastery.java.task.dto.EmployeeDto; +import com.mastery.java.task.service.EmployeeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping("employees") public class EmployeeController { -} + + @Value("${my.host}") + private String host; + + private final EmployeeService service; + + @Autowired + public EmployeeController(EmployeeService service) { + this.service = service; + } + + + @PostMapping + public ResponseEntity create( + @RequestBody @Valid EmployeeDto employeeDto) { + EmployeeDto employeeDtoToReturn = service.create(employeeDto); + return ResponseEntity + .created(URI.create(host + "/employees/" + + employeeDtoToReturn.getEmployeeId())) + .body(employeeDtoToReturn); + } + + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> findAll() { + return ResponseEntity.ok(service.findAll()); + } + + + @GetMapping(value = "/{id}", produces= MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findById(@PathVariable long id) { + return ResponseEntity.ok(service.findById(id)); + } + + + @PutMapping("/{id}") + public ResponseEntity update( + @PathVariable long id, + @RequestBody @Valid EmployeeDto employeeDto) { + return ResponseEntity.ok(service.update(id, employeeDto)); + } + + + @PatchMapping("/{id}") + public ResponseEntity partialUpdate( + @PathVariable long id, + @RequestBody EmployeeDto employeeDto) { + return ResponseEntity.ok(service.partialUpdate(id, employeeDto)); + } + + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable long id) { + service.delete(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/mastery/java/task/service/EmployeeService.java b/src/main/java/com/mastery/java/task/service/EmployeeService.java index df79cd5..f996ff2 100644 --- a/src/main/java/com/mastery/java/task/service/EmployeeService.java +++ b/src/main/java/com/mastery/java/task/service/EmployeeService.java @@ -1,4 +1,20 @@ package com.mastery.java.task.service; -public class EmployeeService { +import com.mastery.java.task.dto.EmployeeDto; + +import java.util.List; + +public interface EmployeeService { + + EmployeeDto create(EmployeeDto employeeDto); + + List findAll(); + + EmployeeDto findById(long id); + + EmployeeDto update(long id, EmployeeDto employeeDto); + + EmployeeDto partialUpdate(long id, EmployeeDto employeeDto); + + void delete(long id); } diff --git a/src/main/java/com/mastery/java/task/service/impl/EmployeeServiceImpl.java b/src/main/java/com/mastery/java/task/service/impl/EmployeeServiceImpl.java new file mode 100644 index 0000000..c0365d3 --- /dev/null +++ b/src/main/java/com/mastery/java/task/service/impl/EmployeeServiceImpl.java @@ -0,0 +1,82 @@ +package com.mastery.java.task.service.impl; + +import com.mastery.java.task.dao.EmployeeDao; +import com.mastery.java.task.dto.EmployeeDto; +import com.mastery.java.task.exeption.EmployeeNotFoundException; +import com.mastery.java.task.mapper.entity_dto_mapper.EmployeeDtoMapper; +import com.mastery.java.task.model.Employee; +import com.mastery.java.task.service.EmployeeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class EmployeeServiceImpl implements EmployeeService { + + private final EmployeeDao employeeDao; + private final EmployeeDtoMapper mapper; + + + @Autowired + public EmployeeServiceImpl(EmployeeDao employeeDao, + EmployeeDtoMapper mapper) { + this.employeeDao = employeeDao; + this.mapper = mapper; + } + + @Override + public EmployeeDto create(EmployeeDto employeeDto) { + Employee employee = mapper.mapToEntity(employeeDto); + + return mapper.mapToDto(employeeDao.create(employee)); + } + + @Override + public List findAll() { + return employeeDao.findAll().stream() + .map(mapper::mapToDto) + .collect(Collectors.toList()); + } + + @Override + public EmployeeDto findById(long id) { + return mapper.mapToDto( + employeeDao.findById(id) + .orElseThrow( + () -> new EmployeeNotFoundException(id))); + } + + @Override + public EmployeeDto update(long id, EmployeeDto employeeDto) { + employeeDao.findById(id) + .orElseThrow(() -> new EmployeeNotFoundException(id)); + + Employee employee = mapper.mapToEntity(employeeDto); + + return mapper.mapToDto(employeeDao.update(id, employee)); + } + + @Override + public EmployeeDto partialUpdate(long id, EmployeeDto employeeDto) { + Optional optionalEmployee = employeeDao.findById(id); + optionalEmployee + .orElseThrow(() -> new EmployeeNotFoundException(id)); + + Employee employee = optionalEmployee.get(); + mapper.update(employeeDto, employee); + + return mapper.mapToDto(employeeDao.update(id, employee)); + } + + @Override + public void delete(long id) { + Optional optionalEmployee = employeeDao.findById(id); + optionalEmployee + .orElseThrow(() -> new EmployeeNotFoundException(id)); + + employeeDao.delete(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fe896d4..22a6c3d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,12 @@ -server.servlet.context-path=/simplewebapp \ No newline at end of file +server.servlet.context-path=/simplewebapp + +spring.datasource.url=jdbc:postgresql://localhost:5432/employeedb +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.username=postgres +spring.datasource.password=admin +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +my.host=http://localhost:8080 + +server.error.whitelabel.enabled=false \ No newline at end of file diff --git a/src/test/java/com/mastery/java/task/dao/EmployeeDaoImplTest.java b/src/test/java/com/mastery/java/task/dao/EmployeeDaoImplTest.java new file mode 100644 index 0000000..2d71eda --- /dev/null +++ b/src/test/java/com/mastery/java/task/dao/EmployeeDaoImplTest.java @@ -0,0 +1,71 @@ +package com.mastery.java.task.dao; + +import com.mastery.java.task.model.Employee; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import static com.mastery.java.task.model.Gender.MALE; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@ActiveProfiles("test") +@Sql(scripts = "classpath:data.sql") +@TestMethodOrder(OrderAnnotation.class) +public class EmployeeDaoImplTest { + + @Autowired + EmployeeDao underTest; + + static Employee e; + + @BeforeAll + static void setUp() { + DateTimeFormatter df = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + e = new Employee(1, "Aliko", "Dangote", 1, "Data scientist", MALE, + LocalDate.parse("02-03-1995", df).atStartOfDay()); + } + + @Test + @Order(1) + void shouldFindById() { + Optional expected = Optional.of(e); + + Optional actual = underTest.findById(1); + + Assertions.assertEquals(actual, expected); + } + + @Test + @Order(2) + void shouldUpdate() { + e.setFirstName("test"); + + underTest.update(1, e); + + Optional actual = underTest.findById(1); + Assertions.assertEquals(e.getFirstName(), actual.get().getFirstName()); + } + + @Test + @Order(3) + void shouldDelete() { + underTest.delete(1); + + Optional actual = underTest.findById(1); + Assertions.assertEquals(Optional.empty(), actual); + } +} diff --git a/src/test/java/com/mastery/java/task/dao/EmployeeDaoTest.java b/src/test/java/com/mastery/java/task/dao/EmployeeDaoTest.java deleted file mode 100644 index accf528..0000000 --- a/src/test/java/com/mastery/java/task/dao/EmployeeDaoTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.mastery.java.task.dao; - -public class EmployeeDaoTest { -} diff --git a/src/test/java/com/mastery/java/task/rest/EmployeeControllerTest.java b/src/test/java/com/mastery/java/task/rest/EmployeeControllerTest.java index e927bdc..b31608a 100644 --- a/src/test/java/com/mastery/java/task/rest/EmployeeControllerTest.java +++ b/src/test/java/com/mastery/java/task/rest/EmployeeControllerTest.java @@ -1,4 +1,41 @@ package com.mastery.java.task.rest; +import com.mastery.java.task.Application; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.PropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = Application.class) +@AutoConfigureMockMvc +@PropertySource("classpath:application.properties") public class EmployeeControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void findByIdShouldReturnExistingGiftCertificate() throws Exception { + MvcResult mvcResult = mockMvc.perform(get("/employees/1")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.firstName").value("Aliko")) + .andReturn(); + + assertEquals("application/json", + mvcResult.getResponse().getContentType()); + } + } diff --git a/src/test/java/com/mastery/java/task/service/EmployeeServiceImplTest.java b/src/test/java/com/mastery/java/task/service/EmployeeServiceImplTest.java new file mode 100644 index 0000000..c3a2b6f --- /dev/null +++ b/src/test/java/com/mastery/java/task/service/EmployeeServiceImplTest.java @@ -0,0 +1,65 @@ +package com.mastery.java.task.service; + +import com.mastery.java.task.dao.EmployeeDao; +import com.mastery.java.task.dto.EmployeeDto; +import com.mastery.java.task.exeption.EmployeeNotFoundException; +import com.mastery.java.task.model.Employee; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import static com.mastery.java.task.model.Gender.MALE; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EmployeeServiceImplTest { + + @Autowired + EmployeeService underTest; + + @MockBean + EmployeeDao dao; + + static Employee e; + static EmployeeDto eDto; + + @BeforeAll + static void setUp() { + DateTimeFormatter df = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + e = new Employee(1, "Aliko", "Dangote", 1, "Data scientist", MALE, + LocalDate.parse("02-03-1995", df).atStartOfDay()); + eDto = new EmployeeDto(1L, "Aliko", "Dangote", 1L, "Data scientist", "MALE", + LocalDate.parse("02-03-1995", df).atStartOfDay()); + + } + + @Test + void shouldCorrectlyMapDtoToEntity() { + underTest.create(eDto); + + Mockito.verify(dao).create(e); + } + + @Test + void shouldThrowExceptionWhenNotFound() { + when(dao.findById(anyLong())).thenReturn(Optional.empty()); + + assertThrows(EmployeeNotFoundException.class, + () -> underTest.findById(3)); + } +} diff --git a/src/test/java/com/mastery/java/task/service/EmployeeServiceTest.java b/src/test/java/com/mastery/java/task/service/EmployeeServiceTest.java deleted file mode 100644 index 4331b3d..0000000 --- a/src/test/java/com/mastery/java/task/service/EmployeeServiceTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.mastery.java.task.service; - -public class EmployeeServiceTest { -} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..91373a5 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:h2:mem:employeedb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 0000000..8caf25f --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,10 @@ +INSERT INTO gender (gender) VALUES + ('male'), + ('female'); + +INSERT INTO employee (first_name, last_name, department_id, job_title, gender_id, date_of_birth) VALUES + ('Aliko', 'Dangote', 1, 'Data scientist', 1, parsedatetime('02-03-1995', 'dd-MM-yyyy')), + ('Sam', 'Kane', 1, 'Systems analyst', 1, parsedatetime('11-06-1990', 'dd-MM-yyyy')), + ('Kate', 'Johnson', 2, 'IT coordinator', 2, parsedatetime('26-09-1992', 'dd-MM-yyyy')), + ('Drake', 'Parker', 3, 'Cloud infrastructure architect', 1, parsedatetime('14-10-1996', 'dd-MM-yyyy')), + ('Samantha', 'Walker', 4, 'Database analyst', 2, parsedatetime('19-08-1992', 'dd-MM-yyyy')); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..54b2804 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS gender; +DROP TABLE IF EXISTS employee; + +CREATE TABLE gender ( + id INT PRIMARY KEY auto_increment, + gender VARCHAR(20) NOT NULL +); + +CREATE TABLE employee ( + employee_id INT AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(250) NOT NULL, + last_name VARCHAR(250) NOT NULL, + department_id INT NOT NULL, + job_title VARCHAR(250) NOT NULL, + gender_id INTEGER NOT NULL DEFAULT 1, + date_of_birth TIMESTAMP NOT NULL, + CONSTRAINT fk_gender_id FOREIGN KEY (gender_id) REFERENCES gender (id) +); \ No newline at end of file diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000..22a6c3d --- /dev/null +++ b/target/classes/application.properties @@ -0,0 +1,12 @@ +server.servlet.context-path=/simplewebapp + +spring.datasource.url=jdbc:postgresql://localhost:5432/employeedb +spring.datasource.driverClassName=org.postgresql.Driver +spring.datasource.username=postgres +spring.datasource.password=admin +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +my.host=http://localhost:8080 + +server.error.whitelabel.enabled=false \ No newline at end of file diff --git a/target/generated-sources/annotations/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapperImpl.java b/target/generated-sources/annotations/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapperImpl.java new file mode 100644 index 0000000..b4ab099 --- /dev/null +++ b/target/generated-sources/annotations/com/mastery/java/task/mapper/entity_dto_mapper/EmployeeDtoMapperImpl.java @@ -0,0 +1,91 @@ +package com.mastery.java.task.mapper.entity_dto_mapper; + +import com.mastery.java.task.dto.EmployeeDto; +import com.mastery.java.task.model.Employee; +import com.mastery.java.task.model.Gender; +import javax.annotation.processing.Generated; +import org.springframework.stereotype.Component; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2021-05-25T15:47:23+0300", + comments = "version: 1.4.2.Final, compiler: javac, environment: Java 14.0.1 (Oracle Corporation)" +) +@Component +public class EmployeeDtoMapperImpl implements EmployeeDtoMapper { + + @Override + public EmployeeDto mapToDto(Employee employee) { + if ( employee == null ) { + return null; + } + + EmployeeDto employeeDto = new EmployeeDto(); + + employeeDto.setEmployeeId( employee.getEmployeeId() ); + employeeDto.setFirstName( employee.getFirstName() ); + employeeDto.setLastName( employee.getLastName() ); + employeeDto.setDepartmentId( employee.getDepartmentId() ); + employeeDto.setJobTitle( employee.getJobTitle() ); + if ( employee.getGender() != null ) { + employeeDto.setGender( employee.getGender().name() ); + } + employeeDto.setDateOfBirth( employee.getDateOfBirth() ); + + return employeeDto; + } + + @Override + public Employee mapToEntity(EmployeeDto dto) { + if ( dto == null ) { + return null; + } + + Employee employee = new Employee(); + + if ( dto.getEmployeeId() != null ) { + employee.setEmployeeId( dto.getEmployeeId() ); + } + employee.setFirstName( dto.getFirstName() ); + employee.setLastName( dto.getLastName() ); + if ( dto.getDepartmentId() != null ) { + employee.setDepartmentId( dto.getDepartmentId() ); + } + employee.setJobTitle( dto.getJobTitle() ); + if ( dto.getGender() != null ) { + employee.setGender( Enum.valueOf( Gender.class, dto.getGender() ) ); + } + employee.setDateOfBirth( dto.getDateOfBirth() ); + + return employee; + } + + @Override + public void update(EmployeeDto dto, Employee employee) { + if ( dto == null ) { + return; + } + + if ( dto.getEmployeeId() != null ) { + employee.setEmployeeId( dto.getEmployeeId() ); + } + if ( dto.getFirstName() != null ) { + employee.setFirstName( dto.getFirstName() ); + } + if ( dto.getLastName() != null ) { + employee.setLastName( dto.getLastName() ); + } + if ( dto.getDepartmentId() != null ) { + employee.setDepartmentId( dto.getDepartmentId() ); + } + if ( dto.getJobTitle() != null ) { + employee.setJobTitle( dto.getJobTitle() ); + } + if ( dto.getGender() != null ) { + employee.setGender( Enum.valueOf( Gender.class, dto.getGender() ) ); + } + if ( dto.getDateOfBirth() != null ) { + employee.setDateOfBirth( dto.getDateOfBirth() ); + } + } +}