Skip to content

Commit

Permalink
Merge pull request #30 from Veselovnd88/feature/43-rest-controller
Browse files Browse the repository at this point in the history
Feature/43 rest controller
  • Loading branch information
Veselovnd88 authored Jan 23, 2024
2 parents e5886a3 + 626ce4a commit 28fe21c
Show file tree
Hide file tree
Showing 29 changed files with 1,006 additions and 35 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'com.vdurmont:emoji-java:5.1.1'
implementation 'org.telegram:telegrambots:6.8.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'


compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@Configuration
@RequiredArgsConstructor
@EnableAspectJAutoProxy
public class Config {
public class AppConfig {

private final CompanyInfoServiceImpl companyInfoService;

Expand Down
20 changes: 20 additions & 0 deletions src/main/java/ru/veselov/companybot/config/OpenApiConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.veselov.companybot.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Интерфейс управления ботом-ассистентом")
.description("Управления отделами, информацией и другими функциями телеграм бота")
.version("v2.0.0"));
}

}
110 changes: 110 additions & 0 deletions src/main/java/ru/veselov/companybot/controller/DivisionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ru.veselov.companybot.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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 ru.veselov.companybot.dto.DivisionDTO;
import ru.veselov.companybot.model.DivisionModel;
import ru.veselov.companybot.service.DivisionService;

import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api/v1/division")
@RequiredArgsConstructor
@Validated
@Tag(name = "Division, Отдел, Департамент", description = "Управление отделами")
public class DivisionController {

private final DivisionService divisionService;

@Operation(summary = "Добавить новый отдел",
description = "Принимает название и описание отдела")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Отдел создан",
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = DivisionModel.class))})
})
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public DivisionModel addDivision(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(schema = @Schema(implementation = DivisionDTO.class),
mediaType = MediaType.APPLICATION_JSON_VALUE
))
@RequestBody @Valid DivisionDTO divisionDTO) {
return divisionService.save(divisionDTO);
}

@Operation(summary = "Получить все отделы",
description = "Выгрузка из базы данных всех отделов")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Отдел успешно выгружены",
content = {@Content(array = @ArraySchema(schema = @Schema(implementation = DivisionModel.class)),
mediaType = MediaType.APPLICATION_JSON_VALUE)})
})
@GetMapping("/all")
public List<DivisionModel> getDivisions() {
return divisionService.findAll();
}

@Operation(summary = "Получить отдел по его id",
description = "Получение информации об отделе по его id")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Отдел успешно выгружены",
content = {@Content(schema = @Schema(implementation = DivisionModel.class),
mediaType = MediaType.APPLICATION_JSON_VALUE)})
})
@GetMapping("/{divisionId}")
public DivisionModel getDivisionById(@PathVariable UUID divisionId) {
return divisionService.findById(divisionId);
}

@Operation(summary = "Обновить отдел",
description = "Обновление наименования и описания отдела")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Отдел обновлен",
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = DivisionModel.class))})
})
@PutMapping("/{divisionId}")
public DivisionModel updateDivision(@PathVariable UUID divisionId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(schema = @Schema(implementation = DivisionDTO.class),
mediaType = MediaType.APPLICATION_JSON_VALUE
))
@RequestBody @Valid DivisionDTO divisionDTO) {
return divisionService.update(divisionId, divisionDTO);
}

@Operation(summary = "Удалить отдел",
description = "Удаление отдела")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Отдел удален")
})
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{divisionId}")
public void deleteDivision(@PathVariable UUID divisionId) {
divisionService.delete(divisionId);
}

}
25 changes: 25 additions & 0 deletions src/main/java/ru/veselov/companybot/dto/DivisionDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.veselov.companybot.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DivisionDTO {

@Schema(description = "Short name of division", example = "Common")
@NotEmpty
@Size(max = 10)
private String name;

@Schema(description = "Description of division", example = "Common questions here")
@NotEmpty
@Size(max = 45)
private String description;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.veselov.companybot.exception;

public class DivisionAlreadyExistsException extends ObjectAlreadyExistsException {

public DivisionAlreadyExistsException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.veselov.companybot.exception;

public class ObjectAlreadyExistsException extends RuntimeException {

public ObjectAlreadyExistsException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ru.veselov.companybot.exception.handler;

public enum ErrorCode {

NOT_FOUND,

VALIDATION,

INTERNAL_SERVER_ERROR,

CONFLICT,

BAD_REQUEST,

FORBIDDEN,

UNAUTHORIZED

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.veselov.companybot.exception.handler;

import lombok.experimental.UtilityClass;

@UtilityClass
public class ErrorMessage {

public static final String ERROR_CODE = "errorCode";

public static final String TIMESTAMP = "timestamp";

public static final String VALIDATION_ERROR = "Validation error";

public static final String VIOLATIONS = "violations";

public static final String OBJECT_NOT_FOUND = "Object not found";

public static final String OBJECT_ALREADY_EXISTS = "Object already exists";

public static final String WRONG_ARGUMENT_PASSED = "Wrong type of argument passed";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package ru.veselov.companybot.exception.handler;

import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import ru.veselov.companybot.exception.ObjectAlreadyExistsException;

import java.time.Instant;
import java.util.List;

@ControllerAdvice
@Slf4j
@ApiResponse(responseCode = "400", description = "Валидация полей объекта не прошла",
content = @Content(
schema = @Schema(implementation = ProblemDetail.class),
examples = @ExampleObject(value = """
{
"type": "about:blank",
"title": "Validation error",
"status": 400,
"detail": "Validation failed",
"instance": "/api/v1/division",
"timestamp": "2024-01-20T13:55:59.666535500Z",
"errorCode": "VALIDATION",
"violations": [
{
"name": "name",
"message": "размер должен находиться в диапазоне от 0 до 10",
"currentValue": "Commonfasdfasdfasdfasdfasdfdf"
}
]
}
"""),
mediaType = MediaType.APPLICATION_JSON_VALUE
))
@ApiResponse(responseCode = "409", description = "Отдел с таким наименованием уже существует",
content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @ExampleObject(value = """
{
"type": "about:blank",
"title": "Object already exists",
"status": 409,
"detail": "Object with name COMMON already exists",
"instance": "/api/v1/????",
"timestamp": "2024-01-20T13:57:29.576908600Z",
"errorCode": "CONFLICT"
}
"""),
schema = @Schema(implementation = ProblemDetail.class))
})
public class RestExceptionHandler {
private static final String LOG_MSG_DETAILS = "[Exception {} with message {}] handled";

@ExceptionHandler(EntityNotFoundException.class)
public ProblemDetail handleEntityNotFoundException(EntityNotFoundException e) {
ProblemDetail problemDetail = createProblemDetail(HttpStatus.NOT_FOUND, e);
problemDetail.setTitle(ErrorMessage.OBJECT_NOT_FOUND);
problemDetail.setProperty(ErrorMessage.ERROR_CODE, ErrorCode.NOT_FOUND.toString());
log.debug(LOG_MSG_DETAILS, e.getClass(), e.getMessage());
return problemDetail;
}

@ExceptionHandler(ObjectAlreadyExistsException.class)
public ProblemDetail handleAlreadyExistsException(ObjectAlreadyExistsException e) {
ProblemDetail problemDetail = createProblemDetail(HttpStatus.CONFLICT, e);
problemDetail.setTitle(ErrorMessage.OBJECT_ALREADY_EXISTS);
problemDetail.setProperty(ErrorMessage.ERROR_CODE, ErrorCode.CONFLICT.toString());
log.debug(LOG_MSG_DETAILS, e.getClass(), e.getMessage());
return problemDetail;
}

@ExceptionHandler(ConstraintViolationException.class)
public ProblemDetail handleConstraintViolationException(ConstraintViolationException e) {
List<ViolationError> violationErrors = e.getConstraintViolations().stream()
.map(v -> new ViolationError(
fieldNameFromPath(v.getPropertyPath().toString()),
v.getMessage(),
formatValidationCurrentValue(v.getInvalidValue())))
.toList();
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
ProblemDetail configuredProblemDetails = setUpValidationDetails(problemDetail, violationErrors);
log.debug(LOG_MSG_DETAILS, e.getClass(), e.getMessage());
return configuredProblemDetails;
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ProblemDetail handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
ProblemDetail problemDetail = createProblemDetail(HttpStatus.BAD_REQUEST, e);
problemDetail.setTitle(ErrorMessage.WRONG_ARGUMENT_PASSED);
problemDetail.setProperty(ErrorMessage.ERROR_CODE, ErrorCode.BAD_REQUEST.toString());
log.debug(LOG_MSG_DETAILS, e.getClass(), e.getMessage());
return problemDetail;
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
final List<ViolationError> violationErrors = e.getBindingResult().getFieldErrors().stream()
.map(error -> new ViolationError(
error.getField(), error.getDefaultMessage(),
formatValidationCurrentValue(error.getRejectedValue())))
.toList();
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problemDetail.setProperty(ErrorMessage.TIMESTAMP, Instant.now());
ProblemDetail configuredProblemDetails = setUpValidationDetails(problemDetail, violationErrors);
log.debug(LOG_MSG_DETAILS, e.getClass(), e.getMessage());
return configuredProblemDetails;
}

private ProblemDetail createProblemDetail(HttpStatus status, Exception e) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.getMessage());
problemDetail.setProperty(ErrorMessage.TIMESTAMP, Instant.now());
return problemDetail;
}

private ProblemDetail setUpValidationDetails(ProblemDetail problemDetail, List<ViolationError> violationErrors) {
problemDetail.setTitle(ErrorMessage.VALIDATION_ERROR);
problemDetail.setProperty(ErrorMessage.ERROR_CODE, ErrorCode.VALIDATION.toString());
problemDetail.setProperty(ErrorMessage.VIOLATIONS, violationErrors);
return problemDetail;
}

private String fieldNameFromPath(String path) {
String[] split = path.split("\\.");
if (split.length > 1) {
return split[split.length - 1];
}
return path;
}

private String formatValidationCurrentValue(Object object) {
if (object == null) {
return "null";
}
if (object.toString().contains(object.getClass().getName())) {
return object.getClass().getSimpleName();
}
return object.toString();
}

}
Loading

0 comments on commit 28fe21c

Please sign in to comment.