commit 70ae2b6511035ad07f1d701723aca9bb34e727be Author: Falmer Date: Mon Feb 2 13:45:43 2026 +0300 Initial diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6fbb869 --- /dev/null +++ b/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '4.0.2' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'ru.ilug' +version = '0.0.1-SNAPSHOT' +description = 'Demo project for Spring Boot' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webmvc' + + implementation 'org.hibernate.orm:hibernate-core:7.2.1.Final' + implementation 'org.hibernate.orm:hibernate-hikaricp:7.2.1.Final' + implementation 'com.zaxxer:HikariCP:7.0.2' + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.0' + + runtimeOnly 'com.h2database:h2:2.4.240' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + testImplementation 'org.springframework.boot:spring-boot-starter-validation-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..61285a6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..19a6bde --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5eba7d7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'book-service' diff --git a/src/main/java/ru/ilug/book_service/HibernateConfiguration.java b/src/main/java/ru/ilug/book_service/HibernateConfiguration.java new file mode 100644 index 0000000..b60b6f6 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/HibernateConfiguration.java @@ -0,0 +1,15 @@ +package ru.ilug.book_service; + +import org.hibernate.SessionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HibernateConfiguration { + + @Bean + public SessionFactory sessionFactory() { + return new org.hibernate.cfg.Configuration().configure().buildSessionFactory(); + } + +} diff --git a/src/main/java/ru/ilug/book_service/UpdatedBookServiceApplication.java b/src/main/java/ru/ilug/book_service/UpdatedBookServiceApplication.java new file mode 100644 index 0000000..7c04d2d --- /dev/null +++ b/src/main/java/ru/ilug/book_service/UpdatedBookServiceApplication.java @@ -0,0 +1,13 @@ +package ru.ilug.book_service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UpdatedBookServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(UpdatedBookServiceApplication.class, args); + } + +} diff --git a/src/main/java/ru/ilug/book_service/data/repository/BookRepositoryImpl.java b/src/main/java/ru/ilug/book_service/data/repository/BookRepositoryImpl.java new file mode 100644 index 0000000..fc12b53 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/data/repository/BookRepositoryImpl.java @@ -0,0 +1,75 @@ +package ru.ilug.book_service.data.repository; + +import lombok.RequiredArgsConstructor; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.query.Query; +import org.springframework.stereotype.Component; +import ru.ilug.book_service.domain.BookRepository; +import ru.ilug.book_service.domain.model.Book; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class BookRepositoryImpl implements BookRepository { + + private final SessionFactory sessionFactory; + + @Override + public List findPageable(int pageSize, int page) { + try (Session session = sessionFactory.openSession()) { + Query query = session.createQuery("FROM Book", Book.class); + query.setFirstResult(pageSize * page); + query.setMaxResults(pageSize); + return query.list(); + } + } + + @Override + public Book findById(long id) { + try (Session session = sessionFactory.openSession()) { + return session.find(Book.class, id); + } + } + + @Override + public Book save(Book book) { + try (Session session = sessionFactory.openSession()) { + Transaction transaction = null; + + try { + transaction = session.beginTransaction(); + book = session.merge(book); + transaction.commit(); + + return book; + } catch (Exception e) { + if (transaction != null) { + transaction.rollback(); + } + throw e; + } + } + } + + @Override + public void deleteById(long id) { + try (Session session = sessionFactory.openSession()) { + Transaction transaction = null; + + try { + transaction = session.beginTransaction(); + Book book = session.find(Book.class, id); + session.remove(book); + transaction.commit(); + } catch (Exception e) { + if (transaction != null) { + transaction.rollback(); + } + throw e; + } + } + } +} diff --git a/src/main/java/ru/ilug/book_service/domain/BookRepository.java b/src/main/java/ru/ilug/book_service/domain/BookRepository.java new file mode 100644 index 0000000..fb98cb7 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/domain/BookRepository.java @@ -0,0 +1,17 @@ +package ru.ilug.book_service.domain; + +import ru.ilug.book_service.domain.model.Book; + +import java.util.List; + +public interface BookRepository { + + List findPageable(int pageSize, int page); + + Book findById(long id); + + Book save(Book book); + + void deleteById(long id); + +} diff --git a/src/main/java/ru/ilug/book_service/domain/BookService.java b/src/main/java/ru/ilug/book_service/domain/BookService.java new file mode 100644 index 0000000..b8bde98 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/domain/BookService.java @@ -0,0 +1,51 @@ +package ru.ilug.book_service.domain; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import ru.ilug.book_service.domain.model.Book; +import ru.ilug.book_service.domain.model.BookUpdateRequest; + +import java.util.List; +import java.util.function.Consumer; + +@Component +@RequiredArgsConstructor +public class BookService { + + private final BookRepository bookRepository; + + public List findPageable(int page) { + return bookRepository.findPageable(5, page); + } + + public Book findById(long bookId) { + return bookRepository.findById(bookId); + } + + public Book createNew(Book book) { + return bookRepository.save(book); + } + + public Book update(long bookId, BookUpdateRequest request) { + Book book = bookRepository.findById(bookId); + updateBookFromUpdateRequest(book, request); + return bookRepository.save(book); + } + + public void deleteById(long bookId) { + bookRepository.deleteById(bookId); + } + + private void updateBookFromUpdateRequest(Book book, BookUpdateRequest request) { + setValueIfNotNull(request.title(), book::setTitle); + setValueIfNotNull(request.author(), book::setAuthor); + setValueIfNotNull(request.publicationYear(), book::setPublicationYear); + setValueIfNotNull(request.genre(), book::setGenre); + } + + private void setValueIfNotNull(T value, Consumer valueSetter) { + if (value != null) { + valueSetter.accept(value); + } + } +} diff --git a/src/main/java/ru/ilug/book_service/domain/model/Book.java b/src/main/java/ru/ilug/book_service/domain/model/Book.java new file mode 100644 index 0000000..ca920cd --- /dev/null +++ b/src/main/java/ru/ilug/book_service/domain/model/Book.java @@ -0,0 +1,32 @@ +package ru.ilug.book_service.domain.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "books") +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String title; + private String author; + @Column(unique = true) + private String isbn; + @Column(name = "publication_year") + private int publicationYear; + private Genre genre; + + public enum Genre { + FICTION, + NON_FICTION, + SCIENCE, + HISTORY, + OTHER + } + +} diff --git a/src/main/java/ru/ilug/book_service/domain/model/BookUpdateRequest.java b/src/main/java/ru/ilug/book_service/domain/model/BookUpdateRequest.java new file mode 100644 index 0000000..6f70ded --- /dev/null +++ b/src/main/java/ru/ilug/book_service/domain/model/BookUpdateRequest.java @@ -0,0 +1,11 @@ +package ru.ilug.book_service.domain.model; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.Range; + +public record BookUpdateRequest(@Nullable @Size(min = 2, max = 100) String title, + @Nullable @Size(min = 2, max = 100) String author, + @Nullable @Range(min = 2000, max = 2026) Integer publicationYear, + @Nullable Book.Genre genre) { +} diff --git a/src/main/java/ru/ilug/book_service/infrastructure/controller/BookController.java b/src/main/java/ru/ilug/book_service/infrastructure/controller/BookController.java new file mode 100644 index 0000000..b4628a5 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/infrastructure/controller/BookController.java @@ -0,0 +1,58 @@ +package ru.ilug.book_service.infrastructure.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.ilug.book_service.domain.BookService; +import ru.ilug.book_service.domain.model.Book; +import ru.ilug.book_service.domain.model.BookUpdateRequest; +import ru.ilug.book_service.infrastructure.dto.BookDto; +import ru.ilug.book_service.infrastructure.mapper.BookMapper; + +import java.util.List; + +@RestController +@RequestMapping("/api/books") +@RequiredArgsConstructor +public class BookController { + + private final BookService bookService; + private final BookMapper bookMapper; + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List books(@RequestParam(required = false, defaultValue = "0") int page) { + return bookService.findPageable(page); + } + + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity bookById(@PathVariable long id) { + Book book = bookService.findById(id); + + if (book == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(book, HttpStatus.OK); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public Book newBook(@Valid @RequestBody BookDto bookDto) { + Book book = bookMapper.toEntity(bookDto); + return bookService.createNew(book); + } + + @PatchMapping(value = "/{id}") + public Book updateBook(@PathVariable long id, + @Valid @RequestBody BookUpdateRequest request) { + return bookService.update(id, request); + } + + @DeleteMapping(value = "/{id}") + public void deleteBook(@PathVariable long id) { + bookService.deleteById(id); + } + +} diff --git a/src/main/java/ru/ilug/book_service/infrastructure/dto/BookDto.java b/src/main/java/ru/ilug/book_service/infrastructure/dto/BookDto.java new file mode 100644 index 0000000..3ff956a --- /dev/null +++ b/src/main/java/ru/ilug/book_service/infrastructure/dto/BookDto.java @@ -0,0 +1,15 @@ +package ru.ilug.book_service.infrastructure.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.Range; +import ru.ilug.book_service.domain.model.Book; + +public record BookDto(@NotNull @Size(min = 2, max = 100) String title, + @NotNull @Size(min = 2, max = 50) String author, + @NotNull @NotBlank @Pattern(regexp = "\\d{3}-\\d-\\d{5}-\\d{3}-\\d") String isbn, + @NotNull @Range(min = 2000, max = 2026) Integer publicationYear, + @NotNull Book.Genre genre) { +} diff --git a/src/main/java/ru/ilug/book_service/infrastructure/mapper/BookMapper.java b/src/main/java/ru/ilug/book_service/infrastructure/mapper/BookMapper.java new file mode 100644 index 0000000..22b4383 --- /dev/null +++ b/src/main/java/ru/ilug/book_service/infrastructure/mapper/BookMapper.java @@ -0,0 +1,14 @@ +package ru.ilug.book_service.infrastructure.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.ReportingPolicy; +import ru.ilug.book_service.domain.model.Book; +import ru.ilug.book_service.infrastructure.dto.BookDto; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING) +public interface BookMapper { + Book toEntity(BookDto bookDto); + + BookDto toBookDto(Book book); +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..a809bea --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=updated-book-service \ No newline at end of file diff --git a/src/main/resources/hibernate.cfg.xml b/src/main/resources/hibernate.cfg.xml new file mode 100644 index 0000000..cdbcfcd --- /dev/null +++ b/src/main/resources/hibernate.cfg.xml @@ -0,0 +1,29 @@ + + + + + + org.hibernate.hikaricp.internal.HikariCPConnectionProvider + + + org.h2.Driver + jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + sa + + + 2 + 10 + 30000 + 20000 + 1800000 + + org.hibernate.dialect.H2Dialect + true + true + create-drop + + + + \ No newline at end of file diff --git a/src/test/java/ru/ilug/book_service/UpdatedBookServiceApplicationTests.java b/src/test/java/ru/ilug/book_service/UpdatedBookServiceApplicationTests.java new file mode 100644 index 0000000..596e52b --- /dev/null +++ b/src/test/java/ru/ilug/book_service/UpdatedBookServiceApplicationTests.java @@ -0,0 +1,13 @@ +package ru.ilug.book_service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UpdatedBookServiceApplicationTests { + + @Test + void contextLoads() { + } + +}