diff --git a/build.gradle.kts b/build.gradle.kts index fc5abdc..679b89f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,8 +19,12 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.projectlombok:lombok:1.18.30") testImplementation("org.springframework.boot:spring-boot-starter-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + annotationProcessor("org.projectlombok:lombok") } tasks.withType { diff --git a/src/main/java/org/javaspringcourse/controller/ArticleController.java b/src/main/java/org/javaspringcourse/controller/ArticleController.java new file mode 100644 index 0000000..57de8cf --- /dev/null +++ b/src/main/java/org/javaspringcourse/controller/ArticleController.java @@ -0,0 +1,20 @@ +package org.javaspringcourse.controller; + +import lombok.RequiredArgsConstructor; +import org.javaspringcourse.dto.ArticleIn; +import org.javaspringcourse.service.ArticleService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/article") +@RequiredArgsConstructor +public class ArticleController { + private final ArticleService service; + + @PostMapping("/create") + @ResponseStatus(HttpStatus.CREATED) + public String register(@RequestBody ArticleIn article) { + return service.create(article); + } +} diff --git a/src/main/java/org/javaspringcourse/dto/ArticleIn.java b/src/main/java/org/javaspringcourse/dto/ArticleIn.java new file mode 100644 index 0000000..550e1d6 --- /dev/null +++ b/src/main/java/org/javaspringcourse/dto/ArticleIn.java @@ -0,0 +1,18 @@ +package org.javaspringcourse.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.javaspringcourse.validation.RussianFullName; +import org.javaspringcourse.validation.Title; + +@Data +public class ArticleIn { + @Title private String title; + @RussianFullName private String author; + @NotBlank private String content; + + @Override + public String toString() { + return title + " (" + author + ")"; + } +} diff --git a/src/main/java/org/javaspringcourse/exception/ArticleExceptionHandler.java b/src/main/java/org/javaspringcourse/exception/ArticleExceptionHandler.java new file mode 100644 index 0000000..394b655 --- /dev/null +++ b/src/main/java/org/javaspringcourse/exception/ArticleExceptionHandler.java @@ -0,0 +1,23 @@ +package org.javaspringcourse.exception; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@RestControllerAdvice +public class ArticleExceptionHandler { + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleBadGatewayException(ConstraintViolationException ex) { + return new ErrorResponse("Неверно заполнены поля.", + ex.getConstraintViolations().stream() + .collect(Collectors.toMap( + violation -> violation.getPropertyPath().toString(), + ConstraintViolation::getMessage))); + } +} diff --git a/src/main/java/org/javaspringcourse/exception/ErrorResponse.java b/src/main/java/org/javaspringcourse/exception/ErrorResponse.java new file mode 100644 index 0000000..10cb3f3 --- /dev/null +++ b/src/main/java/org/javaspringcourse/exception/ErrorResponse.java @@ -0,0 +1,5 @@ +package org.javaspringcourse.exception; + +import java.util.Map; + +public record ErrorResponse(String message, Map errors) {} diff --git a/src/main/java/org/javaspringcourse/service/ArticleService.java b/src/main/java/org/javaspringcourse/service/ArticleService.java new file mode 100644 index 0000000..bc029b2 --- /dev/null +++ b/src/main/java/org/javaspringcourse/service/ArticleService.java @@ -0,0 +1,18 @@ +package org.javaspringcourse.service; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.javaspringcourse.dto.ArticleIn; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@Service +@Validated +public class ArticleService { + public String create(@Valid ArticleIn article) { + var ans = "Creating a new article: " + article.toString(); + log.info(ans); + return ans; + } +} diff --git a/src/main/java/org/javaspringcourse/validation/RussianFullName.java b/src/main/java/org/javaspringcourse/validation/RussianFullName.java new file mode 100644 index 0000000..cf09b7b --- /dev/null +++ b/src/main/java/org/javaspringcourse/validation/RussianFullName.java @@ -0,0 +1,22 @@ +package org.javaspringcourse.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotBlank; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NotBlank +@Constraint(validatedBy = RussianFullNameConstraintValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface RussianFullName { + String message() default "Введённое ФИО не соответствует формату: \"Фамилия Имя Отчество\" (допускаются двойные фамилии)."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/org/javaspringcourse/validation/RussianFullNameConstraintValidator.java b/src/main/java/org/javaspringcourse/validation/RussianFullNameConstraintValidator.java new file mode 100644 index 0000000..562def2 --- /dev/null +++ b/src/main/java/org/javaspringcourse/validation/RussianFullNameConstraintValidator.java @@ -0,0 +1,17 @@ +package org.javaspringcourse.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Arrays; +import java.util.regex.Pattern; + +/**Проверяет, соответствует ли введённая строка Фамилии-Имени-Отчеству в русском алфавите.*/ +public class RussianFullNameConstraintValidator implements ConstraintValidator { + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + var name = s.split(" "); + if (name.length != 3) return false; + return Arrays.stream(name).allMatch(n -> Pattern.matches("^[А-Яа-я]+(-?[А-Яа-я]+)?$", n)); + } +} diff --git a/src/main/java/org/javaspringcourse/validation/Title.java b/src/main/java/org/javaspringcourse/validation/Title.java new file mode 100644 index 0000000..cc36873 --- /dev/null +++ b/src/main/java/org/javaspringcourse/validation/Title.java @@ -0,0 +1,24 @@ +package org.javaspringcourse.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NotNull +@Size(min = 5, max = 40) +@Constraint(validatedBy = {}) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Title { + String message() default "Длина названия должна находиться в диапазоне от 5 до 40."; + + Class[] groups() default {}; + + Class[] payload() default {}; +}