diff --git a/docker-compose.yml b/docker-compose.yml index be96142..c1d5b2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,65 @@ +version: '3.8' + services: + stats-server: + container_name: stats-server + image: stats-server + build: ./stat/stat-server ports: - "9090:9090" + depends_on: + - stats-db + environment: + SPRING_PROFILES_ACTIVE: docker + SPRING_DATASOURCE_URL: jdbc:postgresql://stats-db:5432/stats-service + SPRING_DATASOURCE_USERNAME: user + SPRING_DATASOURCE_PASSWORD: 12345 stats-db: image: postgres:16.1 + container_name: postgres-stat + ports: + - "6541:5432" + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: 12345 + POSTGRES_DB: stats-service + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + interval: 5s + timeout: 5s + retries: 10 ewm-service: + build: + context: ./evm-service + dockerfile: Dockerfile + image: explore-with-me-service + container_name: explore-with-me-service ports: - "8080:8080" + environment: + SPRING_PROFILES_ACTIVE: docker + SPRING_DATASOURCE_URL: jdbc:postgresql://ewm-db:5432/ewm-service + SPRING_DATASOURCE_USERNAME: user + SPRING_DATASOURCE_PASSWORD: 12345 + depends_on: + - ewm-db + - stats-server ewm-db: image: postgres:16.1 + container_name: postgres-service + ports: + - "6542:5432" + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: 12345 + POSTGRES_DB: ewm-service + healthcheck: + test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER + interval: 5s + timeout: 5s + retries: 10 + diff --git a/evm-service/Dockerfile b/evm-service/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/evm-service/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/evm-service/pom.xml b/evm-service/pom.xml new file mode 100644 index 0000000..96ad8a3 --- /dev/null +++ b/evm-service/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + evm-service + + + 5.0.0 + 1.6.3 + 0.2.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.mapstruct + mapstruct + 1.6.3 + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + + + ru.practicum + stat-client + 0.0.1-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + true + + + -Amapstruct.suppressGeneratorTimestamp=true + + + -Amapstruct.suppressGeneratorVersionInfoComment=true + + + -Amapstruct.verbose=true + + + + + + + + + \ No newline at end of file diff --git a/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryAdminController.java b/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryAdminController.java new file mode 100644 index 0000000..6864f84 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryAdminController.java @@ -0,0 +1,35 @@ +package ru.practicum.Category.Controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.DTO.NewCategoryDTO; +import ru.practicum.Category.Service.Admin.CategoryAdminServiceImpl; + +@RestController +@RequestMapping("/admin/categories") +@RequiredArgsConstructor +public class CategoryAdminController { + private final CategoryAdminServiceImpl categoryService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CategoryDTO createCategory(@RequestBody @Valid NewCategoryDTO category) { + return categoryService.addCategory(category); + } + + @DeleteMapping("/{catId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCategory(@PathVariable Integer catId) { + categoryService.deleteCategory(catId); + } + + @PatchMapping("/{catId}") + @ResponseStatus(HttpStatus.OK) + public CategoryDTO updateCategory(@PathVariable Integer catId, @RequestBody @Valid CategoryDTO category) { + return categoryService.updateCategory(catId, category); + } + +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryPublicController.java b/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryPublicController.java new file mode 100644 index 0000000..65cbea9 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Controller/CategoryPublicController.java @@ -0,0 +1,28 @@ +package ru.practicum.Category.Controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.Service.Public.CategoryPublicService; + +import java.util.List; + +@RestController +@RequestMapping("/categories") +@RequiredArgsConstructor +public class CategoryPublicController { + private final CategoryPublicService categoryService; + + @GetMapping("/{catId}") + public CategoryDTO getCategory(@PathVariable Integer catId) { + return categoryService.getCategory(catId); + } + + @GetMapping + public List getCategories(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return categoryService.getCategories(from, size); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Category/DTO/CategoryDTO.java b/evm-service/src/main/java/ru/practicum/Category/DTO/CategoryDTO.java new file mode 100644 index 0000000..fdc9e3d --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/DTO/CategoryDTO.java @@ -0,0 +1,18 @@ +package ru.practicum.Category.DTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CategoryDTO { + private Integer id; + + @NotBlank + @Size(max = 50) + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/Category/DTO/NewCategoryDTO.java b/evm-service/src/main/java/ru/practicum/Category/DTO/NewCategoryDTO.java new file mode 100644 index 0000000..0e0dbfc --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/DTO/NewCategoryDTO.java @@ -0,0 +1,16 @@ +package ru.practicum.Category.DTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NewCategoryDTO { + @NotBlank + @Size(max = 50) + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Mapper/CategoryMapper.java b/evm-service/src/main/java/ru/practicum/Category/Mapper/CategoryMapper.java new file mode 100644 index 0000000..a1d82dd --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Mapper/CategoryMapper.java @@ -0,0 +1,15 @@ +package ru.practicum.Category.Mapper; + +import org.mapstruct.Mapper; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.DTO.NewCategoryDTO; +import ru.practicum.Category.Model.Category; + +@Mapper(componentModel = "spring") +public interface CategoryMapper { + Category toCategory(NewCategoryDTO categoryDTO); + + CategoryDTO toCategoryDTO(Category category); + + Category toCategory(CategoryDTO categoryDTO); +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Model/Category.java b/evm-service/src/main/java/ru/practicum/Category/Model/Category.java new file mode 100644 index 0000000..92b44d1 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Model/Category.java @@ -0,0 +1,22 @@ +package ru.practicum.Category.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "categories") +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "name", length = 50) + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Repository/CategoryRepository.java b/evm-service/src/main/java/ru/practicum/Category/Repository/CategoryRepository.java new file mode 100644 index 0000000..f1634f8 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Repository/CategoryRepository.java @@ -0,0 +1,16 @@ +package ru.practicum.Category.Repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.Category.Model.Category; + +import java.util.Optional; + +public interface CategoryRepository extends JpaRepository { + Category save(Category category); + + void deleteById(Integer id); + + Optional findById(Integer id); + + +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminService.java b/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminService.java new file mode 100644 index 0000000..4609760 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminService.java @@ -0,0 +1,12 @@ +package ru.practicum.Category.Service.Admin; + +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.DTO.NewCategoryDTO; + +public interface CategoryAdminService { + CategoryDTO addCategory(NewCategoryDTO categoryDTO); + + void deleteCategory(Integer catId); + + CategoryDTO updateCategory(int id, CategoryDTO categoryDTO); +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminServiceImpl.java b/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminServiceImpl.java new file mode 100644 index 0000000..3e13534 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Service/Admin/CategoryAdminServiceImpl.java @@ -0,0 +1,60 @@ +package ru.practicum.Category.Service.Admin; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.DTO.NewCategoryDTO; +import ru.practicum.Category.Mapper.CategoryMapper; +import ru.practicum.Category.Model.Category; +import ru.practicum.Category.Repository.CategoryRepository; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.ConflictException; +import ru.practicum.Exception.NotFoundException; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CategoryAdminServiceImpl implements CategoryAdminService { + private final CategoryMapper categoryMapper; + private final CategoryRepository repository; + private final EventRepository eventRepository; + + @Override + public CategoryDTO addCategory(NewCategoryDTO newCategory) { + Category category = categoryMapper.toCategory(newCategory); + return categoryMapper.toCategoryDTO(repository.save(category)); + } + + @Override + public void deleteCategory(Integer catId) { + Category category = repository.findById(catId) + .orElseThrow(() -> new NotFoundException("Category with id " + catId + " not found")); + + List events = eventRepository.findEventsByCategory_Id(catId); + if (!events.isEmpty()) { + throw new ConflictException("Category with id " + catId + " has connected events"); + } + + // Integer categoryEvents = + + repository.deleteById(catId); + } + + @Override + public CategoryDTO updateCategory(int id, CategoryDTO newCategory) { + Category category = repository.findById(id) + .orElseThrow(() -> new NotFoundException("Category with id " + id + " not found")); + + if (category.getName().equals(newCategory.getName())) { + return categoryMapper.toCategoryDTO(category); + } else { + return categoryMapper.toCategoryDTO(repository.save(categoryMapper.toCategory(newCategory))); + } + + + } + + +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicService.java b/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicService.java new file mode 100644 index 0000000..7d614ec --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicService.java @@ -0,0 +1,11 @@ +package ru.practicum.Category.Service.Public; + +import ru.practicum.Category.DTO.CategoryDTO; + +import java.util.List; + +public interface CategoryPublicService { + CategoryDTO getCategory(Integer catId); + + List getCategories(Integer from, Integer size); +} diff --git a/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicServiceImpl.java b/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicServiceImpl.java new file mode 100644 index 0000000..fb00324 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Category/Service/Public/CategoryPublicServiceImpl.java @@ -0,0 +1,40 @@ +package ru.practicum.Category.Service.Public; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.Category.Mapper.CategoryMapper; +import ru.practicum.Category.Model.Category; +import ru.practicum.Category.Repository.CategoryRepository; +import ru.practicum.Exception.NotFoundException; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CategoryPublicServiceImpl implements CategoryPublicService { + private final CategoryRepository repository; + private final CategoryMapper categoryMapper; + + @Override + public CategoryDTO getCategory(Integer catId) { + Category category = repository.findById(catId) + .orElseThrow(() -> new NotFoundException("Category with id " + catId + " not found")); + return categoryMapper.toCategoryDTO(category); + } + + + + @Override + public List getCategories(Integer from, Integer size) { + Pageable pageable = PageRequest.of(from, size, Sort.by("id").ascending()); + Page categories = repository.findAll(pageable); + return categories.getContent().stream() + .map(categoryMapper::toCategoryDTO) + .toList(); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationDTO.java b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationDTO.java new file mode 100644 index 0000000..062239e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationDTO.java @@ -0,0 +1,26 @@ +package ru.practicum.Compilation.CompilationDTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.DTO.EventShortDTO; + +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CompilationDTO { + @NotNull + private Integer id; + + @NotNull + private Boolean pinned; + + @NotBlank + private String title; + + private Set events; +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationsListRequestParams.java b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationsListRequestParams.java new file mode 100644 index 0000000..02ca7bc --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/CompilationsListRequestParams.java @@ -0,0 +1,16 @@ +package ru.practicum.Compilation.CompilationDTO; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; + +@Data +public class CompilationsListRequestParams { + private Boolean pinned; + + @PositiveOrZero + private Integer from = 0; + + @Positive + private Integer size = 10; +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/NewCompilationDTO.java b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/NewCompilationDTO.java new file mode 100644 index 0000000..6286f29 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/NewCompilationDTO.java @@ -0,0 +1,21 @@ +package ru.practicum.Compilation.CompilationDTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NewCompilationDTO { + private ArrayList events; + private Boolean pinned = false; + + @NotBlank + @Size(min = 1, max = 50) + private String title; +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/UpdateCompilationRequest.java b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/UpdateCompilationRequest.java new file mode 100644 index 0000000..9339d0e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/CompilationDTO/UpdateCompilationRequest.java @@ -0,0 +1,20 @@ +package ru.practicum.Compilation.CompilationDTO; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +import java.util.ArrayList; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateCompilationRequest { + private ArrayList events; + private Boolean pinned; + + @Size(min = 1, max = 50) + private String title; +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationAdminController.java b/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationAdminController.java new file mode 100644 index 0000000..24c1cf8 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationAdminController.java @@ -0,0 +1,37 @@ +package ru.practicum.Compilation.Controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.NewCompilationDTO; +import ru.practicum.Compilation.CompilationDTO.UpdateCompilationRequest; +import ru.practicum.Compilation.Service.Admin.CompilationAdminServiceImpl; + +@RestController +@RequestMapping("/admin/compilations") +@RequiredArgsConstructor +public class CompilationAdminController { + private final CompilationAdminServiceImpl compilationService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CompilationDTO addCompilation(@RequestBody @Valid NewCompilationDTO compilationDTO) { + return compilationService.addCompilation(compilationDTO); + + } + + @DeleteMapping("/{compId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCompilation(@PathVariable Integer compId) { + compilationService.deleteCompilation(compId); + } + + @PatchMapping("/{compId}") + public CompilationDTO updateCompilation(@PathVariable Integer compId, + @RequestBody @Valid UpdateCompilationRequest compilationDTO) { + return compilationService.updateCompilation(compId, compilationDTO); + } + +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationPublicController.java b/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationPublicController.java new file mode 100644 index 0000000..81fc643 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Controller/CompilationPublicController.java @@ -0,0 +1,26 @@ +package ru.practicum.Compilation.Controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.CompilationsListRequestParams; +import ru.practicum.Compilation.Service.Public.CompilationPublicServiceImpl; + +import java.util.List; + +@RestController +@RequestMapping("/compilations") +@RequiredArgsConstructor +public class CompilationPublicController { + private final CompilationPublicServiceImpl compilationService; + + @GetMapping("/{compId}") + public CompilationDTO getCompilation(@PathVariable Integer compId) { + return compilationService.getCompilation(compId); + } + + @GetMapping + public List getCompilations(@ModelAttribute CompilationsListRequestParams params) { + return compilationService.getCompilations(params); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Mapper/CompilationMapper.java b/evm-service/src/main/java/ru/practicum/Compilation/Mapper/CompilationMapper.java new file mode 100644 index 0000000..6020c2c --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Mapper/CompilationMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.Compilation.Mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.NewCompilationDTO; +import ru.practicum.Compilation.Model.Compilation; + +@Mapper(componentModel = "spring") +public interface CompilationMapper { + CompilationDTO toDto(Compilation compilation); + + Compilation toCompilation(CompilationDTO dto); + + @Mapping(target = "events", ignore = true) + @Mapping(target = "id", ignore = true) + Compilation toCompilation(NewCompilationDTO compilation); +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Model/Compilation.java b/evm-service/src/main/java/ru/practicum/Compilation/Model/Compilation.java new file mode 100644 index 0000000..221ebbe --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Model/Compilation.java @@ -0,0 +1,35 @@ +package ru.practicum.Compilation.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.Event; + +import java.util.Set; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "compilations") +public class Compilation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @ManyToMany + @JoinTable( + name = "compilation_events", + joinColumns = @JoinColumn(name = "compilation_id"), + inverseJoinColumns = @JoinColumn(name = "event_id") + ) + private Set events; + + @Column(name = "pinned") + private Boolean pinned; + + @Column(name = "title") + private String title; +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Repository/CompilationRepository.java b/evm-service/src/main/java/ru/practicum/Compilation/Repository/CompilationRepository.java new file mode 100644 index 0000000..8a4bc5c --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Repository/CompilationRepository.java @@ -0,0 +1,17 @@ +package ru.practicum.Compilation.Repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.Compilation.Model.Compilation; + +import org.springframework.data.domain.Pageable; +import java.util.Optional; + +public interface CompilationRepository extends JpaRepository { + + Optional findCompilationById(Integer compId); + + Compilation save(Compilation compilation); + + Page findCompilationByPinned(Boolean pinned, Pageable pageable); +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminService.java b/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminService.java new file mode 100644 index 0000000..71b3a9e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminService.java @@ -0,0 +1,14 @@ +package ru.practicum.Compilation.Service.Admin; + +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.NewCompilationDTO; +import ru.practicum.Compilation.CompilationDTO.UpdateCompilationRequest; + + +public interface CompilationAdminService { + CompilationDTO addCompilation(NewCompilationDTO compilationDTO); + + void deleteCompilation(Integer compId); + + CompilationDTO updateCompilation(Integer compId, UpdateCompilationRequest compilationDTO); +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminServiceImpl.java b/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminServiceImpl.java new file mode 100644 index 0000000..764c182 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Service/Admin/CompilationAdminServiceImpl.java @@ -0,0 +1,66 @@ +package ru.practicum.Compilation.Service.Admin; + + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.NewCompilationDTO; +import ru.practicum.Compilation.CompilationDTO.UpdateCompilationRequest; +import ru.practicum.Compilation.Mapper.CompilationMapper; +import ru.practicum.Compilation.Model.Compilation; +import ru.practicum.Compilation.Repository.CompilationRepository; +import ru.practicum.Event.Mapper.EventMapper; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.NotFoundException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class CompilationAdminServiceImpl implements CompilationAdminService { + private final CompilationRepository compilationRepository; + private final EventRepository eventRepository; + private final CompilationMapper compilationMapper; + private final EventMapper eventMapper; + + @Override + public CompilationDTO addCompilation(NewCompilationDTO compilationDTO) { + Compilation compilation = compilationMapper.toCompilation(compilationDTO); + + if (compilationDTO.getEvents() != null) { + Set events = setEvents(compilationDTO.getEvents()); + compilation.setEvents(events); + } + + return compilationMapper.toDto(compilationRepository.save(compilation)); + } + + @Override + public void deleteCompilation(Integer compId) { + compilationRepository.delete(checkCompilation(compId)); + } + + @Override + public CompilationDTO updateCompilation(Integer compId, UpdateCompilationRequest compilationDTO) { + Compilation compilation = checkCompilation(compId); + + if (compilationDTO.getEvents() != null) { + Set events = setEvents(compilationDTO.getEvents()); + compilation.setEvents(events); + } + + return compilationMapper.toDto(compilationRepository.save(compilation)); + } + + private Compilation checkCompilation(Integer compId) { + return compilationRepository.findCompilationById(compId) + .orElseThrow(() -> new NotFoundException("Compilation with id " + compId + " not found")); + } + + private Set setEvents(List eventIds) { + return new HashSet<>(eventRepository.findEventsByIdIn(eventIds)); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicService.java b/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicService.java new file mode 100644 index 0000000..3b76595 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicService.java @@ -0,0 +1,12 @@ +package ru.practicum.Compilation.Service.Public; + +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.CompilationsListRequestParams; + +import java.util.List; + +public interface CompilationPublicService { + CompilationDTO getCompilation(Integer compId); + + List getCompilations(CompilationsListRequestParams params); +} diff --git a/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicServiceImpl.java b/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicServiceImpl.java new file mode 100644 index 0000000..f8a14b9 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Compilation/Service/Public/CompilationPublicServiceImpl.java @@ -0,0 +1,116 @@ +package ru.practicum.Compilation.Service.Public; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import ru.practicum.Compilation.CompilationDTO.CompilationDTO; +import ru.practicum.Compilation.CompilationDTO.CompilationsListRequestParams; +import ru.practicum.Compilation.Mapper.CompilationMapper; +import ru.practicum.Compilation.Model.Compilation; +import ru.practicum.Compilation.Repository.CompilationRepository; +import ru.practicum.Event.Mapper.EventMapper; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.NotFoundException; +import ru.practicum.StatsClient; +import ru.practicum.ViewStatsDTO; + +import java.time.LocalDateTime; +import java.util.*; + +import static java.lang.Integer.parseInt; + +@Service +@RequiredArgsConstructor +public class CompilationPublicServiceImpl implements CompilationPublicService { + private final CompilationRepository compilationRepository; + private final CompilationMapper compilationMapper; + private final EventRepository eventRepository; + private final StatsClient statsClient; + private final EventMapper eventMapper; + + @Override + public CompilationDTO getCompilation(Integer compId) { + Compilation compilation = checkCompilation(compId); + CompilationDTO compilationDTO = compilationMapper.toDto(compilation); + + List events = compilation.getEvents().stream().toList(); + + if ((events != null) && (!events.isEmpty())) { + Map views = getEvents(events); + compilationDTO.getEvents().stream() + .peek(eventShortDTO -> eventShortDTO.setViews(views.get(eventShortDTO.getId()))); + } + + return compilationDTO; + } + + @Override + public List getCompilations(CompilationsListRequestParams params) { + int page = (int) Math.floor((double) params.getFrom() / params.getSize()); + Pageable pageable = PageRequest.of(page, params.getSize()); + Page compilations = params.getPinned() == null + ? compilationRepository.findAll(pageable) + : compilationRepository.findCompilationByPinned(params.getPinned(), pageable); + + List events = compilations.stream() + .flatMap(c -> c.getEvents().stream()) + .distinct() + .toList(); + + List compilationDtos = compilations.getContent().stream() + .map(compilationMapper::toDto) + .toList(); + + + if ((events != null) && (!events.isEmpty())) { + Map views = getEvents(events); + compilationDtos = compilationDtos.stream().peek(compilationDTO -> { + compilationDTO.getEvents().stream() + .peek(eventShortDTO -> eventShortDTO.setViews(views.get(eventShortDTO.getId()) + )); + } + ).toList(); + } + + return compilationDtos; + + } + + private Compilation checkCompilation(Integer compId) { + return compilationRepository.findCompilationById(compId) + .orElseThrow(() -> new NotFoundException("Compilation with id " + compId + " not found")); + } + + private Map getEvents(List events) { + if (events == null) { + return Collections.emptyMap(); + } + + List ids = events.stream() + .map(Event::getId) + .toList(); + List events1 = eventRepository.findEventsByIdIn(ids); + + List uris = events.stream() + .map(event -> "/events/" + event.getId()) + .distinct() + .toList(); + LocalDateTime start = LocalDateTime.now().minusMonths(6); + LocalDateTime end = LocalDateTime.now().plusMinutes(1); + List viewStatsDTOS = statsClient.viewStats(start, end, uris, true); + + Map views = new HashMap<>(); + if (!viewStatsDTOS.isEmpty()) { + for (ViewStatsDTO viewStatsDTO : viewStatsDTOS) { + int eventId = parseInt(viewStatsDTO.getUri().substring("/events/".length())); + views.put(eventId, viewStatsDTO.getHits()); + } + } + + return views; + } +} + diff --git a/evm-service/src/main/java/ru/practicum/Event/Controller/EventAdminController.java b/evm-service/src/main/java/ru/practicum/Event/Controller/EventAdminController.java new file mode 100644 index 0000000..24f166c --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Controller/EventAdminController.java @@ -0,0 +1,32 @@ +package ru.practicum.Event.Controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.SearchEventsDTO; +import ru.practicum.Event.DTO.UpdateEventAdminRequest; +import ru.practicum.Event.Service.Admin.EventAdminService; + +import java.util.List; + +@RestController +@RequestMapping("/admin/events") +@RequiredArgsConstructor +public class EventAdminController { + private final EventAdminService eventAdminService; + + @GetMapping + public List getEvents(@ModelAttribute @Valid SearchEventsDTO dto) { + return eventAdminService.getEvents(dto); + } + + @PatchMapping("/{eventId}") + @ResponseStatus(HttpStatus.OK) + public EventFullDTO updateEvent(@PathVariable Integer eventId, + @RequestBody @Valid UpdateEventAdminRequest request) { + return eventAdminService.updateEvent(eventId, request); + } + +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Controller/EventPrivateController.java b/evm-service/src/main/java/ru/practicum/Event/Controller/EventPrivateController.java new file mode 100644 index 0000000..fb82c2c --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Controller/EventPrivateController.java @@ -0,0 +1,59 @@ +package ru.practicum.Event.Controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Event.DTO.*; +import ru.practicum.Event.Service.Private.EventPrivateServiceImpl; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; + +import java.util.List; + +@RestController +@RequestMapping("/users/{userId}/events") +@RequiredArgsConstructor +public class EventPrivateController { + private final EventPrivateServiceImpl eventPrivateService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public EventFullDTO addEvent(@PathVariable Integer userId, @RequestBody @Valid NewEventDTO event) { + return eventPrivateService.addEvent(userId, event); + } + + @GetMapping + public List getEvents(@PathVariable Integer userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + return eventPrivateService.getEvents(userId, from, size); + } + + @GetMapping("/{eventId}") + public EventFullDTO getEvent(@PathVariable Integer userId, @PathVariable Integer eventId) { + return eventPrivateService.getEvent(userId, eventId); + } + + @PatchMapping("/{eventId}") + public EventFullDTO updateEvent(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody @Valid UpdateEventUserRequest event) { + return eventPrivateService.updateEvent(userId, eventId, event); + } + + @GetMapping("/{eventId}/requests") + public List getUserRequests(@PathVariable Integer userId, + @PathVariable Integer eventId) { + return eventPrivateService.getUserRequests(userId, eventId); + } + + @PatchMapping("/{eventId}/requests") + public EventRequestStatusUpdateResult updateRequests(@RequestBody @Valid EventRequestStatusUpdateRequest request, + @PathVariable Integer userId, + @PathVariable Integer eventId) { + return eventPrivateService.updateRequests(request, userId, eventId); + } + +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Controller/EventPublicController.java b/evm-service/src/main/java/ru/practicum/Event/Controller/EventPublicController.java new file mode 100644 index 0000000..d559408 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Controller/EventPublicController.java @@ -0,0 +1,31 @@ +package ru.practicum.Event.Controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.EventPublicParams; +import ru.practicum.Event.DTO.EventShortDTO; +import ru.practicum.Event.Service.Public.EventPublicServiceImpl; + +import java.util.List; + + +@RestController +@RequestMapping("/events") +@RequiredArgsConstructor +public class EventPublicController { + private final EventPublicServiceImpl eventPublicService; + + @GetMapping("/{id}") + public EventFullDTO findEventById(@PathVariable Integer id, HttpServletRequest request) { + return eventPublicService.findEventById(id, request); + } + + @GetMapping + public List findAllEvents(@ModelAttribute @Valid EventPublicParams params, + HttpServletRequest request) { + return eventPublicService.findAllEvents(params, request); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/EventFullDTO.java b/evm-service/src/main/java/ru/practicum/Event/DTO/EventFullDTO.java new file mode 100644 index 0000000..71d8c1e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/EventFullDTO.java @@ -0,0 +1,53 @@ +package ru.practicum.Event.DTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.StatsRequestDTO; +import ru.practicum.User.DTO.UserShortDTO; +import ru.practicum.Event.Model.Location; +import ru.practicum.Event.Model.State; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EventFullDTO { + private Integer id; + + private String annotation; + + private CategoryDTO category; + + private Integer confirmedRequests; + + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime createdOn; + + private String description; + + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime eventDate; + + private UserShortDTO initiator; + + private Location location; + + private Boolean paid; + + private Integer participantLimit; + + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime publishedOn = LocalDateTime.now(); + + private Boolean requestModeration; + + private State state; + + private String title; + + private Integer views; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/EventPublicParams.java b/evm-service/src/main/java/ru/practicum/Event/DTO/EventPublicParams.java new file mode 100644 index 0000000..0224c91 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/EventPublicParams.java @@ -0,0 +1,37 @@ +package ru.practicum.Event.DTO; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import ru.practicum.StatsRequestDTO; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EventPublicParams { + private String text; + public ArrayList categories; + private Boolean paid; + + @DateTimeFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime rangeStart; + + @DateTimeFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime rangeEnd; + + private Boolean onlyAvailable = false; + + private String sort; + + @PositiveOrZero + private Integer from = 0; + + @Positive + private Integer size = 10; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateRequest.java b/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateRequest.java new file mode 100644 index 0000000..b7bade6 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateRequest.java @@ -0,0 +1,24 @@ +package ru.practicum.Event.DTO; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.State; + +import java.util.ArrayList; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EventRequestStatusUpdateRequest { + + @NotNull + @NotEmpty + private ArrayList requestIds; + + @NotNull + private State status; + +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateResult.java b/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateResult.java new file mode 100644 index 0000000..215b2be --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/EventRequestStatusUpdateResult.java @@ -0,0 +1,16 @@ +package ru.practicum.Event.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EventRequestStatusUpdateResult { + private List confirmedRequests; + private List rejectedRequests; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/EventShortDTO.java b/evm-service/src/main/java/ru/practicum/Event/DTO/EventShortDTO.java new file mode 100644 index 0000000..b268a70 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/EventShortDTO.java @@ -0,0 +1,43 @@ +package ru.practicum.Event.DTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Category.DTO.CategoryDTO; +import ru.practicum.StatsRequestDTO; +import ru.practicum.User.DTO.UserShortDTO; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EventShortDTO { + private Integer id; + + @NotBlank + private String annotation; + + @NotNull + private CategoryDTO category; + + private Integer confirmedRequests; + + @NotNull + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime eventDate; + + @NotNull + private UserShortDTO initiator; + + @NotNull + private Boolean paid; + + @NotBlank + private String title; + + private Integer views; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/NewEventDTO.java b/evm-service/src/main/java/ru/practicum/Event/DTO/NewEventDTO.java new file mode 100644 index 0000000..8d840c2 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/NewEventDTO.java @@ -0,0 +1,48 @@ +package ru.practicum.Event.DTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.Location; +import ru.practicum.StatsRequestDTO; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NewEventDTO { + @NotBlank + @Size(min = 20, max = 2000) + private String annotation; + + @NotNull + @JsonProperty("category") + private Integer categoryId; + + @NotBlank + @Size(min = 20, max = 7000) + private String description; + + @NotNull + @Future + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime eventDate; + + @NotNull + private Location location; + + private Boolean paid = false; + + @PositiveOrZero + private Integer participantLimit = 0; + + private Boolean requestModeration = true; + + @NotBlank + @Size(min = 3, max = 120) + private String title; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/SearchEventsDTO.java b/evm-service/src/main/java/ru/practicum/Event/DTO/SearchEventsDTO.java new file mode 100644 index 0000000..f037783 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/SearchEventsDTO.java @@ -0,0 +1,34 @@ +package ru.practicum.Event.DTO; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import ru.practicum.Event.Model.State; +import ru.practicum.StatsRequestDTO; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SearchEventsDTO { + private List users; + private List states; + private List categories; + + @DateTimeFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime rangeStart = LocalDateTime.now(); + + @DateTimeFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime rangeEnd; + + @PositiveOrZero + private Integer from = 0; + + @Positive + private Integer size = 10; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventAdminRequest.java b/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventAdminRequest.java new file mode 100644 index 0000000..52d08f6 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventAdminRequest.java @@ -0,0 +1,45 @@ +package ru.practicum.Event.DTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.Location; +import ru.practicum.Event.Model.State; +import ru.practicum.StatsRequestDTO; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateEventAdminRequest { + + @Size(min = 20, max = 2000) + private String annotation; + + private Integer category; + + @Size(min = 20, max = 7000) + private String description; + + @Future + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime eventDate; + + private Location location; + + private Boolean paid; + + private Integer participantLimit; + + private Boolean requestModeration; + + private State stateAction; + + @Size(min = 3, max = 120) + private String title; + +} diff --git a/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventUserRequest.java b/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventUserRequest.java new file mode 100644 index 0000000..b605ab0 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/DTO/UpdateEventUserRequest.java @@ -0,0 +1,46 @@ +package ru.practicum.Event.DTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.Location; +import ru.practicum.Event.Model.State; +import ru.practicum.StatsRequestDTO; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateEventUserRequest { + + @Size(min = 20, max = 2000) + private String annotation; + + private Integer categoryId; + + @Size(min = 20, max = 7000) + private String description; + + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + @Future + private LocalDateTime eventDate; + + private Location location; + + private Boolean paid; + + @PositiveOrZero + private Integer participantLimit; + + private Boolean requestModeration; + + private State stateAction; + + @Size(min = 3, max = 120) + private String title; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Mapper/EventMapper.java b/evm-service/src/main/java/ru/practicum/Event/Mapper/EventMapper.java new file mode 100644 index 0000000..74afdec --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Mapper/EventMapper.java @@ -0,0 +1,21 @@ +package ru.practicum.Event.Mapper; + +import org.mapstruct.*; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.EventShortDTO; +import ru.practicum.Event.DTO.NewEventDTO; +import ru.practicum.Event.DTO.UpdateEventAdminRequest; +import ru.practicum.Event.Model.Event; + +@Mapper(componentModel = "spring") +public interface EventMapper { + Event toEvent(NewEventDTO event); + + EventShortDTO toEventShortDTO(Event event); + + EventFullDTO toEventFullDTO(Event event); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + @Mapping(target = "category", ignore = true) + Event toEvent(UpdateEventAdminRequest request, @MappingTarget Event event); +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Model/Event.java b/evm-service/src/main/java/ru/practicum/Event/Model/Event.java new file mode 100644 index 0000000..2b81c22 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Model/Event.java @@ -0,0 +1,70 @@ +package ru.practicum.Event.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Category.Model.Category; +import ru.practicum.User.Model.User; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "events") +public class Event { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "title", length = 200) + private String title; + + @Column(name = "annotation", length = 2000) + private String annotation; + + @ManyToOne(fetch = FetchType.EAGER) + private Category category; + + @Column(name = "confirmed_requests") + private Integer confirmedRequests = 0; + + @Column(name = "created_on") + private LocalDateTime createdOn = LocalDateTime.now(); + + @Column(name = "description", length = 7000) + private String description; + + @Column(name = "event_date") + private LocalDateTime eventDate; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "initiator_id") + private User initiator; + + @Embedded + private Location location; + + @Column(name = "paid") + private Boolean paid; + + @Column(name = "participant_limit") + private Integer participantLimit; + + @Column(name = "published_on") + private LocalDateTime publishedOn; + + @Column(name = "request_moderation") + private Boolean requestModeration; + + @Column(name = "state_id") + @Enumerated(EnumType.ORDINAL) + private State state; + + @Column(name = "views") + private Integer views = 0; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Model/Location.java b/evm-service/src/main/java/ru/practicum/Event/Model/Location.java new file mode 100644 index 0000000..f97e67e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Model/Location.java @@ -0,0 +1,17 @@ +package ru.practicum.Event.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Embeddable +public class Location { + + private Float lat; + + private Float lon; +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Model/State.java b/evm-service/src/main/java/ru/practicum/Event/Model/State.java new file mode 100644 index 0000000..33b5d70 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Model/State.java @@ -0,0 +1,15 @@ +package ru.practicum.Event.Model; + + +public enum State { + PUBLISHED, + CONFIRMED, + REJECTED, + PENDING, + PUBLISH_EVENT, + REJECT_EVENT, + CANCEL_REVIEW, + SEND_TO_REVIEW, + CANCELED + +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Repository/EventRepository.java b/evm-service/src/main/java/ru/practicum/Event/Repository/EventRepository.java new file mode 100644 index 0000000..22782e2 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Repository/EventRepository.java @@ -0,0 +1,26 @@ +package ru.practicum.Event.Repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Model.State; + +import java.util.List; +import java.util.Optional; + +public interface EventRepository extends JpaRepository { + Optional findEventById(Integer eventId); + + Optional findEventsByIdAndState(Integer id, State state); + + Page getEventByInitiator_Id(Integer initiatorId, Pageable pageable); + + List findEventsByIdIn(List ids); + + List findEventsByCategory_Id(Integer categoryId); + + Page findAll(Specification spec, Pageable pageable); + +} \ No newline at end of file diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminService.java b/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminService.java new file mode 100644 index 0000000..1b4a0c9 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminService.java @@ -0,0 +1,13 @@ +package ru.practicum.Event.Service.Admin; + +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.SearchEventsDTO; +import ru.practicum.Event.DTO.UpdateEventAdminRequest; + +import java.util.List; + +public interface EventAdminService { + List getEvents(SearchEventsDTO searchEventsDTO); + + EventFullDTO updateEvent(Integer eventId, UpdateEventAdminRequest request); +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminServiceImpl.java b/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminServiceImpl.java new file mode 100644 index 0000000..b5200e9 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Admin/EventAdminServiceImpl.java @@ -0,0 +1,123 @@ +package ru.practicum.Event.Service.Admin; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import ru.practicum.Category.Repository.CategoryRepository; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.SearchEventsDTO; +import ru.practicum.Event.DTO.UpdateEventAdminRequest; +import ru.practicum.Event.Mapper.EventMapper; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Model.State; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.ConflictException; +import ru.practicum.Exception.ValidationException; +import ru.practicum.User.Repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class EventAdminServiceImpl implements EventAdminService { + private final EventRepository eventRepository; + private final EventMapper eventMapper; + private final CategoryRepository categoryRepository; + private final UserRepository userRepository; + + @Override + public List getEvents(SearchEventsDTO dto) { + if (dto.getRangeStart() != null && dto.getRangeEnd() != null) { + if (dto.getRangeStart().isAfter(dto.getRangeEnd())) { + throw new ValidationException("Range start is after range end"); + } + } + + Specification spec = getEventSpecification(dto); + Pageable pageable = PageRequest.of(dto.getFrom(), dto.getSize()); + + List events = eventRepository.findAll(spec, pageable).getContent(); + + return events.stream() + .map(eventMapper::toEventFullDTO) + .toList(); + } + + @Override + public EventFullDTO updateEvent(Integer eventId, UpdateEventAdminRequest request) { + Event event = eventRepository.findEventById(eventId) + .orElseThrow(() -> new IllegalArgumentException("Event not found")); + + if (request.getEventDate() != null) { + if (event.getEventDate().isBefore(LocalDateTime.now().plusHours(1))) { + throw new ConflictException("Event date should be 1 horse after creation date"); + } + } + + if (request.getStateAction() != null) { + switch (request.getStateAction()) { + case PUBLISH_EVENT -> { + if (event.getState().equals(State.PENDING)) { + event.setState(State.PUBLISHED); + event.setPublishedOn(LocalDateTime.now()); + eventRepository.save(event); + } else { + throw new ConflictException("Event state should be PENDING"); + } + } + + case REJECT_EVENT -> { + if (!event.getState().equals(State.PUBLISHED)) { + event.setState(State.REJECTED); + eventRepository.save(event); + } else { + throw new ConflictException("Event state should be not PUBLISHED"); + } + } + } + } + + Event updatedEvent = eventMapper.toEvent(request, event); + + return eventMapper.toEventFullDTO(eventRepository.save(updatedEvent)); + } + + private Specification getEventSpecification(SearchEventsDTO event) { + Specification spec = Specification.where(null); + + if (Objects.nonNull(event.getUsers()) && !event.getUsers().isEmpty()) { + spec = spec.and((root, query, cb) -> root.get("initiator").get("id").in( + event.getUsers().stream().filter(Objects::nonNull).toList())); + } + + if (Objects.nonNull(event.getCategories()) && !event.getCategories().isEmpty()) { + spec = spec.and((root, query, builder) -> + root.get("category").get("id").in(event.getCategories().stream() + .filter(Objects::nonNull) + .toList())); + } + + if (Objects.nonNull(event.getStates()) && !event.getStates().isEmpty()) { + spec = spec.and((root, query, cb) -> + root.get("state").in(event.getStates().stream() + .filter(Objects::nonNull) + .toList())); + } + + if (Objects.nonNull(event.getRangeStart())) { + spec = spec.and((root, query, cb) -> + cb.greaterThanOrEqualTo(root.get("eventDate"), event.getRangeStart())); + } + + if (Objects.nonNull(event.getRangeEnd())) { + spec = spec.and((root, query, cb) -> + cb.lessThanOrEqualTo(root.get("eventDate"), event.getRangeEnd())); + } + + return spec; + } +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateService.java b/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateService.java new file mode 100644 index 0000000..5ac76da --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateService.java @@ -0,0 +1,20 @@ +package ru.practicum.Event.Service.Private; + +import ru.practicum.Event.DTO.*; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; + +import java.util.List; + +public interface EventPrivateService { + EventFullDTO addEvent(Integer userId, NewEventDTO event); + + EventFullDTO getEvent(Integer userId, Integer eventId); + + EventFullDTO updateEvent(Integer userId, Integer eventId, UpdateEventUserRequest event); + + List getUserRequests(Integer userId, Integer eventId); + + EventRequestStatusUpdateResult updateRequests(EventRequestStatusUpdateRequest request, Integer userId, Integer eventId); + + List getEvents(Integer userId, Integer from, Integer size); +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateServiceImpl.java b/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateServiceImpl.java new file mode 100644 index 0000000..d5c52bb --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Private/EventPrivateServiceImpl.java @@ -0,0 +1,176 @@ +package ru.practicum.Event.Service.Private; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import ru.practicum.Category.Model.Category; +import ru.practicum.Category.Repository.CategoryRepository; +import ru.practicum.Event.DTO.*; +import ru.practicum.Event.Mapper.EventMapper; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.ConflictException; +import ru.practicum.Exception.NotFoundException; +import ru.practicum.Exception.ValidationException; +import ru.practicum.Request.Mapper.RequestMapper; +import ru.practicum.Request.Model.Request; +import ru.practicum.Request.Repository.RequestRepository; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; +import ru.practicum.User.Model.User; +import ru.practicum.User.Repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static ru.practicum.Event.Model.State.*; + +@Service +@RequiredArgsConstructor +public class EventPrivateServiceImpl implements EventPrivateService { + private final EventRepository eventRepository; + private final EventMapper eventMapper; + private final UserRepository userRepository; + private final CategoryRepository categoryRepository; + private final RequestRepository requestRepository; + private final RequestMapper requestMapper; + + @Override + public EventFullDTO addEvent(Integer userId, NewEventDTO event) { + User user = checkUser(userId); + Category category = checkCategory(event.getCategoryId()); + + Event newEvent = eventMapper.toEvent(event); + checkDates(newEvent.getCreatedOn(), event.getEventDate()); + newEvent.setInitiator(user); + newEvent.setCategory(category); + newEvent.setState(PENDING); + return eventMapper.toEventFullDTO(eventRepository.save(newEvent)); + } + + @Override + public EventFullDTO getEvent(Integer userId, Integer eventId) { + Event event = checkEvent(eventId); + int confirmedRequests = requestRepository.findRequestsByEvent_IdAndStatus(eventId, CONFIRMED).size(); + return eventMapper.toEventFullDTO(event); + } + + @Override + public EventFullDTO updateEvent(Integer userId, Integer eventId, UpdateEventUserRequest event) { + Event eventFound = checkEvent(eventId); + User user = checkUser(userId); + + if (eventFound.getState().equals(PUBLISHED)) { + throw new ConflictException("Event with id " + eventId + " could not be changed"); + } + + if (event.getStateAction() != null) { + switch (event.getStateAction()) { + case CANCEL_REVIEW -> eventFound.setState(CANCELED); + case SEND_TO_REVIEW -> eventFound.setState(PENDING); + } + } + + if (!userId.equals(eventFound.getInitiator().getId())) { + throw new ConflictException("Initiator id mismatch"); + } + + if (event.getCategoryId() != null) { + Category category = checkCategory(event.getCategoryId()); + eventFound.setCategory(category); + } + + if (event.getEventDate() != null) { + checkDates(eventFound.getCreatedOn(), event.getEventDate()); + eventFound.setEventDate(event.getEventDate()); + } + + return eventMapper.toEventFullDTO(eventRepository.save(eventFound)); + } + + @Override + public List getUserRequests(Integer userId, Integer eventId) { + checkUser(userId); + checkEvent(eventId); + + return requestRepository.findRequestsByEvent_IdAndEvent_Initiator_Id(eventId, userId).stream() + .map(requestMapper::toParticipationRequestDto) + .toList(); + } + + @Override + public EventRequestStatusUpdateResult updateRequests(EventRequestStatusUpdateRequest request, + Integer userId, Integer eventId) { + checkUser(userId); + Event event = checkEvent(eventId); + List requests = requestRepository.findAllById(request.getRequestIds()); + + if (Objects.equals(event.getParticipantLimit(), event.getConfirmedRequests())) { + throw new ConflictException("ConfirmedRequests limit exceeded"); + } + + List confirmedRequests = new ArrayList<>(); + List rejectedRequests = new ArrayList<>(); + + for (Request request1 : requests) { + if (!request1.getStatus().equals(PENDING)) { + throw new ConflictException("States should be PENDING"); + } + + if (request.getStatus().equals(CONFIRMED)) { + if (event.getParticipantLimit() > event.getConfirmedRequests()) { + request1.setStatus(CONFIRMED); + requestRepository.save(request1); + confirmedRequests.add(requestMapper.toParticipationRequestDto(request1)); + event.setConfirmedRequests(event.getConfirmedRequests() + 1); + eventRepository.save(event); + } else { + request1.setStatus(REJECTED); + requestRepository.save(request1); + rejectedRequests.add(requestMapper.toParticipationRequestDto(request1)); + } + } else { + request1.setStatus(REJECTED); + requestRepository.save(request1); + rejectedRequests.add(requestMapper.toParticipationRequestDto(request1)); + } + } + + return new EventRequestStatusUpdateResult(confirmedRequests, rejectedRequests); + } + + @Override + public List getEvents(Integer userId, Integer from, Integer size) { + checkUser(userId); + Pageable pageable = PageRequest.of(from, size); + Page events = eventRepository.getEventByInitiator_Id(userId, pageable); + return events.getContent().stream() + .map(eventMapper::toEventShortDTO) + .toList(); + } + + private void checkDates(LocalDateTime createdOn, LocalDateTime eventDate) { + if (eventDate.isBefore(createdOn) || + eventDate.minusHours(2L).isBefore(createdOn)) { + throw new ValidationException("Event date should be 2 horse after creation date"); + } + } + + private User checkUser(Integer userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("User with id " + userId + " not found")); + } + + private Category checkCategory(Integer categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException("Category with id " + categoryId + " not found")); + } + + private Event checkEvent(Integer eventId) { + return eventRepository.findEventById(eventId) + .orElseThrow(() -> new NotFoundException("Event with id " + eventId + " not found")); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicService.java b/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicService.java new file mode 100644 index 0000000..db38b40 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicService.java @@ -0,0 +1,14 @@ +package ru.practicum.Event.Service.Public; + +import jakarta.servlet.http.HttpServletRequest; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.EventPublicParams; +import ru.practicum.Event.DTO.EventShortDTO; + +import java.util.List; + +public interface EventPublicService { + EventFullDTO findEventById(Integer id, HttpServletRequest request); + + List findAllEvents(EventPublicParams params, HttpServletRequest request); +} diff --git a/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicServiceImpl.java b/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicServiceImpl.java new file mode 100644 index 0000000..044d22f --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Event/Service/Public/EventPublicServiceImpl.java @@ -0,0 +1,168 @@ +package ru.practicum.Event.Service.Public; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import ru.practicum.Category.Repository.CategoryRepository; +import ru.practicum.Event.DTO.EventFullDTO; +import ru.practicum.Event.DTO.EventPublicParams; +import ru.practicum.Event.DTO.EventShortDTO; +import ru.practicum.Event.Mapper.EventMapper; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Model.State; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.NotFoundException; +import ru.practicum.Exception.ValidationException; +import ru.practicum.Request.Repository.RequestRepository; +import ru.practicum.StatsClient; +import ru.practicum.ViewStatsDTO; + +import java.time.LocalDateTime; +import java.util.*; + +import static java.lang.Integer.parseInt; + + +@Service +@RequiredArgsConstructor +public class EventPublicServiceImpl implements EventPublicService { + private final EventRepository eventRepository; + private final EventMapper eventMapper; + private final CategoryRepository categoryRepository; + private final RequestRepository requestRepository; + private final StatsClient statsClient; + + @Override + public EventFullDTO findEventById(Integer id, HttpServletRequest request) { + Event event = eventRepository.findEventsByIdAndState(id, State.PUBLISHED) + .orElseThrow(() -> new NotFoundException("Event with id " + id + " not found")); + + statsClient.createHit(request, "ewm-main-service"); + + List uris = List.of(request.getRequestURI()); + Map views = getStats(uris, request); + event.setViews(views.get(event.getId())); + + return eventMapper.toEventFullDTO(event); + } + + @Override + public List findAllEvents(EventPublicParams params, HttpServletRequest request) { + statsClient.createHit(request, "ewm-main-service"); + + if (params.getRangeStart() != null || params.getRangeEnd() != null) { + if (params.getRangeStart().isAfter(params.getRangeEnd())) { + throw new ValidationException("Range start is after range end"); + } + } + + Specification spec = eventSpecification(params); + + Sort sort = null; + if (params.getSort() != null) { + switch (params.getSort()) { + case "EVENT_DATE" -> sort = Sort.by(Sort.Direction.DESC, "eventDate"); + case "VIEWS" -> sort = Sort.by(Sort.Direction.ASC, "views"); + } + } else { + sort = Sort.by(Sort.Direction.DESC, "eventDate"); + } + + + Pageable pageable = PageRequest.of(params.getFrom(), params.getSize(), sort); + + List events = eventRepository.findAll(spec, pageable).getContent().stream() + .map(eventMapper::toEventShortDTO) + .toList(); + + List uris = events.stream() + .map(event -> { + return request.getRequestURI() + "/" + event.getId(); + }) + .toList(); + + if (!uris.isEmpty()) { + Map views = getStats(uris, request); + events.stream() + .peek(event -> { + if (views.containsKey(event.getId())) { + event.setViews(views.get(event.getId())); + } + }) + .toList(); + } + + return events; + } + + private Map getStats(List uris, HttpServletRequest request) { + if (uris.isEmpty()) { + return Collections.emptyMap(); + } + + LocalDateTime start = LocalDateTime.now().minusMonths(6); + LocalDateTime end = LocalDateTime.now().plusMinutes(1); + List viewStatsDTOS = statsClient.viewStats(start, end, uris, true); + + Map eventViews = new HashMap<>(); + + for (ViewStatsDTO viewStatsDTO : viewStatsDTOS) { + int index = viewStatsDTO.getUri().lastIndexOf('/') + 1; + int id = parseInt(viewStatsDTO.getUri().substring(index)); + eventViews.put(id, viewStatsDTO.getHits()); + } + + return eventViews; + } + + private Specification eventSpecification(EventPublicParams params) { + Specification spec = Specification.where(null); + + if (Objects.nonNull(params.getText()) && !params.getText().isBlank()) { + String search = "%" + params.getText().toLowerCase() + "%"; + spec = spec.and((root, query, cb) -> + cb.or( + cb.like(cb.lower(root.get("annotation")), search), + cb.like(cb.lower(root.get("description")), search) + )); + } + + if (Objects.nonNull(params.getCategories()) && !params.getCategories().isEmpty()) { + spec = spec.and((root, query, cb) -> + root.get("category").get("id").in( + params.getCategories())); + } + + if (Objects.nonNull(params.getPaid())) { + spec = spec.and((root, query, cb) -> + cb.equal(root.get("paid"), params.getPaid())); + } + + if (Objects.nonNull(params.getOnlyAvailable())) { + spec = spec.and((root, query, cb) -> + cb.or( + cb.equal(root.get("participantLimit"), 0), + cb.lessThan( + root.get("confirmedRequests"), + root.get("participantLimit") + ) + )); + } + + if (Objects.nonNull(params.getRangeStart())) { + spec = spec.and((root, query, cb) -> + cb.greaterThanOrEqualTo(root.get("eventDate"), params.getRangeStart())); + } + + if (Objects.nonNull(params.getRangeEnd())) { + spec = spec.and((root, query, cb) -> + cb.lessThanOrEqualTo(root.get("eventDate"), params.getRangeEnd())); + } + + return spec; + } +} diff --git a/evm-service/src/main/java/ru/practicum/EvmServiceApp.java b/evm-service/src/main/java/ru/practicum/EvmServiceApp.java new file mode 100644 index 0000000..78f5ae5 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/EvmServiceApp.java @@ -0,0 +1,11 @@ +package ru.practicum; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EvmServiceApp { + public static void main(String[] args) { + SpringApplication.run(EvmServiceApp.class, args); + } +} \ No newline at end of file diff --git a/evm-service/src/main/java/ru/practicum/Exception/ConflictException.java b/evm-service/src/main/java/ru/practicum/Exception/ConflictException.java new file mode 100644 index 0000000..cd46f5b --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/ConflictException.java @@ -0,0 +1,11 @@ +package ru.practicum.Exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException() { + super(); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Exception/ErrorResponse.java b/evm-service/src/main/java/ru/practicum/Exception/ErrorResponse.java new file mode 100644 index 0000000..eefbe00 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/ErrorResponse.java @@ -0,0 +1,19 @@ +package ru.practicum.Exception; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ErrorResponse { + private HttpStatus status; + private String message; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timestamp; + +} diff --git a/evm-service/src/main/java/ru/practicum/Exception/EvmErrorHandler.java b/evm-service/src/main/java/ru/practicum/Exception/EvmErrorHandler.java new file mode 100644 index 0000000..7d23cde --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/EvmErrorHandler.java @@ -0,0 +1,64 @@ +package ru.practicum.Exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; + +@RestControllerAdvice +@Slf4j +public class EvmErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException(final RuntimeException e) { + log.error("500 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage(), timestamp); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationException(final ValidationException e) { + log.error("400 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage(), timestamp); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFoundException(final NotFoundException e) { + log.error("404 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage(), timestamp); + } + + @ExceptionHandler(ConflictException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ErrorResponse handleConflictException(final ConflictException e) { + log.error("409 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.CONFLICT, e.getMessage(), timestamp); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMissingPathVariableException(final MissingServletRequestParameterException e) { + log.error("400 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage(), timestamp); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ErrorResponse handSQLException(final DataIntegrityViolationException e) { + log.error("409 {}", e.getMessage(), e); + LocalDateTime timestamp = LocalDateTime.now(); + return new ErrorResponse(HttpStatus.CONFLICT, e.getMessage(), timestamp); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Exception/NotFoundException.java b/evm-service/src/main/java/ru/practicum/Exception/NotFoundException.java new file mode 100644 index 0000000..982e60a --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/NotFoundException.java @@ -0,0 +1,11 @@ +package ru.practicum.Exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException() { + super(); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Exception/UniqueException.java b/evm-service/src/main/java/ru/practicum/Exception/UniqueException.java new file mode 100644 index 0000000..ca6b3a6 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/UniqueException.java @@ -0,0 +1,11 @@ +package ru.practicum.Exception; + +public class UniqueException extends RuntimeException { + public UniqueException(final String message) { + super(message); + } + + public UniqueException() { + super(); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Exception/ValidationException.java b/evm-service/src/main/java/ru/practicum/Exception/ValidationException.java new file mode 100644 index 0000000..06bd106 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Exception/ValidationException.java @@ -0,0 +1,11 @@ +package ru.practicum.Exception; + +public class ValidationException extends RuntimeException { + public ValidationException() { + super(); + } + + public ValidationException(String message) { + super(message); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Controller/RequestPrivateController.java b/evm-service/src/main/java/ru/practicum/Request/Controller/RequestPrivateController.java new file mode 100644 index 0000000..8c25286 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Controller/RequestPrivateController.java @@ -0,0 +1,35 @@ +package ru.practicum.Request.Controller; + +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; +import ru.practicum.Request.Service.RequestPrivateServiceImpl; + +import java.util.List; + +@RestController +@RequestMapping("/users/{userId}/requests") +@RequiredArgsConstructor +public class RequestPrivateController { + private final RequestPrivateServiceImpl requestService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ParticipationRequestDto addRequest(@PathVariable @Positive Integer userId, + @RequestParam @Positive Integer eventId) { + return requestService.addRequest(userId, eventId); + } + + @PatchMapping("/{requestId}/cancel") + public ParticipationRequestDto cancelRequest(@PathVariable @Positive Integer userId, + @PathVariable @Positive Integer requestId) { + return requestService.cancelRequest(userId, requestId); + } + + @GetMapping + List getRequests(@PathVariable @Positive Integer userId) { + return requestService.getRequests(userId); + } +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Mapper/RequestMapper.java b/evm-service/src/main/java/ru/practicum/Request/Mapper/RequestMapper.java new file mode 100644 index 0000000..e232dad --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Mapper/RequestMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.Request.Mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import ru.practicum.Request.Model.Request; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; + +@Mapper(componentModel = "spring") +public interface RequestMapper { + + @Mapping(target = "event", source = "event.id") + @Mapping(target = "requester", source = "requester.id") + ParticipationRequestDto toParticipationRequestDto(Request request); + + @Mapping(target = "event", ignore = true) + @Mapping(target = "requester", ignore = true) + Request toRequest(ParticipationRequestDto participationRequestDto); +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Model/Request.java b/evm-service/src/main/java/ru/practicum/Request/Model/Request.java new file mode 100644 index 0000000..3e5b1d5 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Model/Request.java @@ -0,0 +1,37 @@ +package ru.practicum.Request.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Model.State; +import ru.practicum.User.Model.User; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "requests") +public class Request { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "created") + private final LocalDateTime created = LocalDateTime.now(); + + @OneToOne + private Event event; + + @OneToOne(fetch = FetchType.EAGER) + private User requester; + + @Column(name = "state_id") + @Enumerated(EnumType.ORDINAL) + private State status; +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Repository/RequestRepository.java b/evm-service/src/main/java/ru/practicum/Request/Repository/RequestRepository.java new file mode 100644 index 0000000..43d0400 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Repository/RequestRepository.java @@ -0,0 +1,20 @@ +package ru.practicum.Request.Repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.Event.Model.State; +import ru.practicum.Request.Model.Request; + +import java.util.List; +import java.util.Optional; + +public interface RequestRepository extends JpaRepository { + Optional findRequestById(Integer requestId); + + List findRequestsByRequester_Id(Integer requesterId); + + List findRequestsByEvent_IdAndEvent_Initiator_Id(Integer eventId, Integer eventInitiatorId); + + List findRequestsByEvent_IdAndStatus(Integer eventId, State status); + + Request findRequestsByEvent_IdAndRequester_Id(Integer eventId, Integer requesterId); +} diff --git a/evm-service/src/main/java/ru/practicum/Request/RequestDTO/ParticipationRequestDto.java b/evm-service/src/main/java/ru/practicum/Request/RequestDTO/ParticipationRequestDto.java new file mode 100644 index 0000000..1e3edc8 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/RequestDTO/ParticipationRequestDto.java @@ -0,0 +1,24 @@ +package ru.practicum.Request.RequestDTO; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.practicum.Event.Model.State; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ParticipationRequestDto { + + private Integer id; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime created = LocalDateTime.now(); + + private Integer event; + private Integer requester; + private State status; +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateService.java b/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateService.java new file mode 100644 index 0000000..29be91e --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateService.java @@ -0,0 +1,13 @@ +package ru.practicum.Request.Service; + +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; + +import java.util.List; + +public interface RequestPrivateService { + ParticipationRequestDto addRequest(Integer userId, Integer eventId); + + ParticipationRequestDto cancelRequest(Integer userId, Integer requestId); + + List getRequests(Integer userId); +} diff --git a/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateServiceImpl.java b/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateServiceImpl.java new file mode 100644 index 0000000..7cfbc06 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/Request/Service/RequestPrivateServiceImpl.java @@ -0,0 +1,88 @@ +package ru.practicum.Request.Service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.Event.Model.Event; +import ru.practicum.Event.Model.State; +import ru.practicum.Event.Repository.EventRepository; +import ru.practicum.Exception.ConflictException; +import ru.practicum.Exception.NotFoundException; +import ru.practicum.Request.Mapper.RequestMapper; +import ru.practicum.Request.Model.Request; +import ru.practicum.Request.Repository.RequestRepository; +import ru.practicum.Request.RequestDTO.ParticipationRequestDto; +import ru.practicum.User.Model.User; +import ru.practicum.User.Repository.UserRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RequestPrivateServiceImpl implements RequestPrivateService { + private final RequestRepository requestRepository; + private final EventRepository eventRepository; + private final UserRepository userRepository; + private final RequestMapper requestMapper; + + @Override + public ParticipationRequestDto addRequest(Integer userId, Integer eventId) { + Request request = new Request(); + + Event event = eventRepository.findEventById(eventId) + .orElseThrow(() -> new NotFoundException("Event not found")); + + User user = checkUser(userId); + + if (requestRepository.findRequestsByEvent_IdAndRequester_Id(eventId, userId) != null) { + throw new ConflictException("Request already exists"); + } + + if (!event.getState().equals(State.PUBLISHED)) { + throw new ConflictException("Event is not published"); + } else if (event.getInitiator().getId().equals(userId)) { + throw new ConflictException("Requester and initiator could not be the same."); + } + + if (event.getParticipantLimit().equals(event.getConfirmedRequests()) && event.getParticipantLimit() != 0) { + throw new ConflictException("Request limit exceeded"); + } + + if (!(event.getRequestModeration()) || (event.getParticipantLimit() == 0)) { + request.setStatus(State.CONFIRMED); + event.setConfirmedRequests(event.getConfirmedRequests() + 1); + eventRepository.save(event); + } else { + request.setStatus(State.PENDING); + } + + request.setEvent(event); + request.setRequester(user); + + return requestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + @Override + public ParticipationRequestDto cancelRequest(Integer userId, Integer requestId) { + Request request = requestRepository.findRequestById(requestId) + .orElseThrow(() -> new NotFoundException("Request with id " + requestId + " not found")); + + checkUser(userId); + + request.setStatus(State.CANCELED); + + return requestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + @Override + public List getRequests(Integer userId) { + checkUser(userId); + return requestRepository.findRequestsByRequester_Id(userId).stream() + .map(requestMapper::toParticipationRequestDto) + .toList(); + } + + private User checkUser(Integer userId) { + return userRepository.findUserById(userId) + .orElseThrow(() -> new NotFoundException("User with id " + userId + " not found")); + } +} diff --git a/evm-service/src/main/java/ru/practicum/User/Controller/UserAdminController.java b/evm-service/src/main/java/ru/practicum/User/Controller/UserAdminController.java new file mode 100644 index 0000000..3ee9657 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Controller/UserAdminController.java @@ -0,0 +1,47 @@ +package ru.practicum.User.Controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.User.DTO.NewUserRequest; +import ru.practicum.User.DTO.UserDTO; +import ru.practicum.User.DTO.UsersListRequest; +import ru.practicum.User.Service.UserAdminServiceImpl; + +import java.util.List; + +@RestController +@RequestMapping("/admin/users") +@RequiredArgsConstructor +public class UserAdminController { + private final UserAdminServiceImpl userAdminService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserDTO addUser(@RequestBody @Valid NewUserRequest newUser) { + return userAdminService.addUser(newUser); + } + + @GetMapping("/{userId}") + @ResponseStatus(HttpStatus.OK) + public UserDTO getUserById(@PathVariable Integer userId) { + return userAdminService.getUserById(userId); + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getUsers(@RequestParam(required = false) List ids, + @RequestParam(defaultValue = "0") @PositiveOrZero int from, + @RequestParam(defaultValue = "10") @Positive int size) { + return userAdminService.getUsers(new UsersListRequest(ids, from, size)); + } + + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable Integer userId) { + userAdminService.deleteUserById(userId); + } +} diff --git a/evm-service/src/main/java/ru/practicum/User/DTO/NewUserRequest.java b/evm-service/src/main/java/ru/practicum/User/DTO/NewUserRequest.java new file mode 100644 index 0000000..9e2e7f4 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/DTO/NewUserRequest.java @@ -0,0 +1,23 @@ +package ru.practicum.User.DTO; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NewUserRequest { + @Email + @NotBlank + @Size(min = 6, max = 254) + private String email; + + @NotBlank + @Size(min = 2, max = 250) + private String name; + +} diff --git a/evm-service/src/main/java/ru/practicum/User/DTO/UserDTO.java b/evm-service/src/main/java/ru/practicum/User/DTO/UserDTO.java new file mode 100644 index 0000000..552d020 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/DTO/UserDTO.java @@ -0,0 +1,21 @@ +package ru.practicum.User.DTO; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserDTO { + private Integer id; + + @NotBlank + @Email + private String email; + + @NotBlank + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/User/DTO/UserShortDTO.java b/evm-service/src/main/java/ru/practicum/User/DTO/UserShortDTO.java new file mode 100644 index 0000000..6f8c646 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/DTO/UserShortDTO.java @@ -0,0 +1,18 @@ +package ru.practicum.User.DTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserShortDTO { + @NotNull + private Integer id; + + @NotBlank + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/User/DTO/UsersListRequest.java b/evm-service/src/main/java/ru/practicum/User/DTO/UsersListRequest.java new file mode 100644 index 0000000..c194ba8 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/DTO/UsersListRequest.java @@ -0,0 +1,9 @@ +package ru.practicum.User.DTO; + +import java.util.List; + +public record UsersListRequest( + List ids, + Integer from, + Integer size) { +} diff --git a/evm-service/src/main/java/ru/practicum/User/Mapper/UserMapper.java b/evm-service/src/main/java/ru/practicum/User/Mapper/UserMapper.java new file mode 100644 index 0000000..6ee6e70 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Mapper/UserMapper.java @@ -0,0 +1,16 @@ +package ru.practicum.User.Mapper; + +import org.mapstruct.Mapper; +import ru.practicum.User.DTO.NewUserRequest; +import ru.practicum.User.DTO.UserDTO; +import ru.practicum.User.DTO.UserShortDTO; +import ru.practicum.User.Model.User; + +@Mapper(componentModel = "spring") +public interface UserMapper { + User toUser(NewUserRequest user); + + UserDTO toUserDTO(User user); + + UserShortDTO toUserShortDTO(User user); +} diff --git a/evm-service/src/main/java/ru/practicum/User/Model/User.java b/evm-service/src/main/java/ru/practicum/User/Model/User.java new file mode 100644 index 0000000..9e08ef5 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Model/User.java @@ -0,0 +1,24 @@ +package ru.practicum.User.Model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "email", length = 500) + private String email; + + @Column(name = "name", length = 250) + private String name; +} diff --git a/evm-service/src/main/java/ru/practicum/User/Repository/UserRepository.java b/evm-service/src/main/java/ru/practicum/User/Repository/UserRepository.java new file mode 100644 index 0000000..4f34f21 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Repository/UserRepository.java @@ -0,0 +1,17 @@ +package ru.practicum.User.Repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.User.Model.User; + +import java.util.Collection; +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + User save(User newUser); + + Optional findUserById(Integer id); + + Page findByIdIn(Collection ids, Pageable pageable); +} diff --git a/evm-service/src/main/java/ru/practicum/User/Service/UserAdminService.java b/evm-service/src/main/java/ru/practicum/User/Service/UserAdminService.java new file mode 100644 index 0000000..75a8beb --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Service/UserAdminService.java @@ -0,0 +1,17 @@ +package ru.practicum.User.Service; + +import ru.practicum.User.DTO.NewUserRequest; +import ru.practicum.User.DTO.UserDTO; +import ru.practicum.User.DTO.UsersListRequest; + +import java.util.List; + +public interface UserAdminService { + UserDTO addUser(NewUserRequest newUser); + + void deleteUserById(Integer id); + + UserDTO getUserById(Integer id); + + List getUsers(UsersListRequest request); +} diff --git a/evm-service/src/main/java/ru/practicum/User/Service/UserAdminServiceImpl.java b/evm-service/src/main/java/ru/practicum/User/Service/UserAdminServiceImpl.java new file mode 100644 index 0000000..378cbf0 --- /dev/null +++ b/evm-service/src/main/java/ru/practicum/User/Service/UserAdminServiceImpl.java @@ -0,0 +1,59 @@ +package ru.practicum.User.Service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.Exception.NotFoundException; +import ru.practicum.User.DTO.NewUserRequest; +import ru.practicum.User.DTO.UserDTO; +import ru.practicum.User.DTO.UsersListRequest; +import ru.practicum.User.Mapper.UserMapper; +import ru.practicum.User.Model.User; +import ru.practicum.User.Repository.UserRepository; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class UserAdminServiceImpl implements UserAdminService { + private final UserRepository userRepository; + private final UserMapper userMapper; + + @Override + public UserDTO addUser(NewUserRequest newUser) { + return userMapper.toUserDTO(userRepository.save(userMapper.toUser(newUser))); + } + + @Override + public void deleteUserById(Integer id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User with id " + id + " not found")); + + userRepository.delete(user); + } + + @Override + public UserDTO getUserById(Integer id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User with id " + id + " not found")); + + return userMapper.toUserDTO(user); + } + + @Override + public List getUsers(UsersListRequest request) { + int page = request.from() / request.size(); + Pageable pageable = PageRequest.of(page, request.size(), Sort.by("id").ascending()); + + List users = request.ids() == null || request.ids().isEmpty() + ? userRepository.findAll(pageable).getContent() + : userRepository.findByIdIn(request.ids(), pageable).getContent(); + + return users.stream() + .map(userMapper::toUserDTO) + .toList(); + } +} diff --git a/evm-service/src/main/resources/application-docker.yml b/evm-service/src/main/resources/application-docker.yml new file mode 100644 index 0000000..14693c9 --- /dev/null +++ b/evm-service/src/main/resources/application-docker.yml @@ -0,0 +1,30 @@ +server: + port: 8080 + +stat-server: + url: http://stats-server:9090 + +spring: + jpa: + hibernate.ddl-auto: none + properties.hibernate.format_sql: true + + sql: + init: + mode: always + datasource: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://ewm-service:5432/ewm-service + username: user + password: 12345 + application: + name: evm-service + +logging: + level: + org: + springframework: + orm.jpa: INFO + transaction: INFO + interceptor: TRACE + JpaTransactionManager: DEBUG \ No newline at end of file diff --git a/evm-service/src/main/resources/application.yml b/evm-service/src/main/resources/application.yml new file mode 100644 index 0000000..e2e1cd1 --- /dev/null +++ b/evm-service/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8080 + +stat-server: + url: http://localhost:9090 + +spring: + jpa: + hibernate.ddl-auto: none + properties.hibernate.format_sql: true + + sql: + init: + mode: always + datasource: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/ewm-service + username: user + password: 12345 + application: + name: evm-service + +logging: + level: + org: + springframework: + orm.jpa: INFO + transaction: INFO + interceptor: TRACE + JpaTransactionManager: DEBUG diff --git a/evm-service/src/main/resources/schema.sql b/evm-service/src/main/resources/schema.sql new file mode 100644 index 0000000..97956c5 --- /dev/null +++ b/evm-service/src/main/resources/schema.sql @@ -0,0 +1,66 @@ +DROP TABLE IF EXISTS users, categories, events, requests, compilations, compilation_events; + +CREATE TABLE IF NOT EXISTS users ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(250) NOT NULL, + email VARCHAR(500) NOT NULL UNIQUE, + CONSTRAINT UQ_USER_EMAIL UNIQUE (email) + ); + + +CREATE TABLE IF NOT EXISTS categories ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + CONSTRAINT UQ_CATEGORY_NAME UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS state ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(50) +); + +CREATE TABLE IF NOT EXISTS events ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + title VARCHAR(205) NOT NULL, + annotation VARCHAR(2000) NOT NULL, + category_id INT NOT NULL, + confirmed_requests INT, + created_on TIMESTAMP WITHOUT TIME ZONE, + description VARCHAR(7000), + event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + initiator_id INT NOT NULL, + lat INT NOT NULL, + lon INT NOT NULL, + paid BOOLEAN, + participant_limit INT, + published_on TIMESTAMP WITHOUT TIME ZONE, + request_moderation BOOLEAN, + state_id INT, + views INT, + CONSTRAINT fk_events_to_categories FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE, + CONSTRAINT fk_events_to_users FOREIGN KEY (initiator_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS compilations ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + pinned BOOLEAN, + title VARCHAR(150) NOT NULL +); + +CREATE TABLE IF NOT EXISTS requests ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + created TIMESTAMP WITHOUT TIME ZONE, + event_id INT NOT NULL, + requester_id INT NOT NULL, + state_id INT, + CONSTRAINT fk_requests_to_events FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE, + CONSTRAINT fk_requests_to_users FOREIGN KEY (requester_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS compilation_events ( + compilation_id INT NOT NULL, + event_id INT NOT NULL, + CONSTRAINT fk_compilation_events_to_compilations FOREIGN KEY (compilation_id) REFERENCES compilations (id) ON DELETE CASCADE, + CONSTRAINT fk_compilation_events_to_events FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE +); + diff --git a/pom.xml b/pom.xml index b15acb2..c2e49de 100644 --- a/pom.xml +++ b/pom.xml @@ -12,13 +12,19 @@ Explore With Me - ru.practicum + + evm-service + stat + + + ru.practicum explore-with-me 0.0.1-SNAPSHOT pom 21 + 21 UTF-8 diff --git a/stat/Dockerfile b/stat/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/stat/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/stat/pom.xml b/stat/pom.xml new file mode 100644 index 0000000..81221fe --- /dev/null +++ b/stat/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + stat + pom + + + stat-client + stat-dto + stat-server + + + \ No newline at end of file diff --git a/stat/stat-client/Dockerfile b/stat/stat-client/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/stat/stat-client/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/stat/stat-client/pom.xml b/stat/stat-client/pom.xml new file mode 100644 index 0000000..b3c1366 --- /dev/null +++ b/stat/stat-client/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + ru.practicum + stat + 0.0.1-SNAPSHOT + + + stat-client + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + compile + + + + \ No newline at end of file diff --git a/stat/stat-client/src/main/java/ru/practicum/StatsClient.java b/stat/stat-client/src/main/java/ru/practicum/StatsClient.java new file mode 100644 index 0000000..fef47fe --- /dev/null +++ b/stat/stat-client/src/main/java/ru/practicum/StatsClient.java @@ -0,0 +1,102 @@ +package ru.practicum; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.common.lang.Nullable; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class StatsClient { + private final String serverUrl; + private final RestTemplate restTemplate; + + @Autowired + public StatsClient(@Value(value = "${stat-server.url:http://stat-server:9090}") String serverUrl, + RestTemplateBuilder builder) { + this.serverUrl = serverUrl; + this.restTemplate = builder.build(); + } + + public ResponseEntity createHit(HttpServletRequest request, String appName) { + String uri = request.getRequestURI(); + EndpointHitDTO dto = getEndpointHitDTO(request, appName); + return post(serverUrl + "/hit", dto); + + } + + public List viewStats(LocalDateTime start, LocalDateTime end, + List uris, Boolean unique) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(StatsRequestDTO.DATE_FORMAT); + Map params = new HashMap<>(); + params.put("start", start.format(formatter)); + params.put("end", end.format(formatter)); + params.put("uris", uris != null ? String.join(",", uris) : ""); + params.put("unique", unique != null ? unique : false); + + ResponseEntity obj = get(serverUrl + "/stats", params); + + ObjectMapper mapper = new ObjectMapper(); + List views = null; + + if (obj.getStatusCode().is2xxSuccessful() && obj.getBody() != null) { + try { + views = mapper.readValue( + mapper.writeValueAsString(obj.getBody()), new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + return views; + } + + private ResponseEntity get(String path, Map parameters) { + return makeAndSendRequest(HttpMethod.GET, path, parameters, null); + } + + private ResponseEntity post(String path, Object body) { + return makeAndSendRequest(HttpMethod.POST, path, null, body); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, + @Nullable Map parameters, + @Nullable T body) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(path); + if (parameters != null) { + parameters.forEach(builder::queryParam); + } + + HttpEntity entity = new HttpEntity<>(body, headers); + + return restTemplate.exchange(builder.build().encode().toUri(), + method, entity, Object.class); + } + + private EndpointHitDTO getEndpointHitDTO(HttpServletRequest request, String appName) { + return EndpointHitDTO.builder() + .app(appName) + .uri(request.getRequestURI()) + .ip(request.getRemoteAddr()) + .timestamp(LocalDateTime.now()) + .build(); + } + +} diff --git a/stat/stat-dto/Dockerfile b/stat/stat-dto/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/stat/stat-dto/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/stat/stat-dto/pom.xml b/stat/stat-dto/pom.xml new file mode 100644 index 0000000..0bb3dfe --- /dev/null +++ b/stat/stat-dto/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + ru.practicum + stat + 0.0.1-SNAPSHOT + + + stat-dto + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + + + org.projectlombok + lombok + 1.18.30 + + + + + + + + + \ No newline at end of file diff --git a/stat/stat-dto/src/main/java/ru/practicum/EndpointHitDTO.java b/stat/stat-dto/src/main/java/ru/practicum/EndpointHitDTO.java new file mode 100644 index 0000000..17d2cb3 --- /dev/null +++ b/stat/stat-dto/src/main/java/ru/practicum/EndpointHitDTO.java @@ -0,0 +1,29 @@ +package ru.practicum; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class EndpointHitDTO { + + @NotBlank + private String app; + + @NotBlank + private String uri; + + @NotBlank + private String ip; + + @JsonFormat(pattern = StatsRequestDTO.DATE_FORMAT) + private LocalDateTime timestamp; +} diff --git a/stat/stat-dto/src/main/java/ru/practicum/StatsRequestDTO.java b/stat/stat-dto/src/main/java/ru/practicum/StatsRequestDTO.java new file mode 100644 index 0000000..50dec47 --- /dev/null +++ b/stat/stat-dto/src/main/java/ru/practicum/StatsRequestDTO.java @@ -0,0 +1,29 @@ +package ru.practicum; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@AllArgsConstructor +@Builder +public class StatsRequestDTO { + public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + @DateTimeFormat(pattern = DATE_FORMAT) + @NotNull(message = "Start date is required") + private final LocalDateTime start; + + @DateTimeFormat(pattern = DATE_FORMAT) + @NotNull(message = "End date is required") + private final LocalDateTime end; + + private List uris; + private Boolean unique; + +} diff --git a/stat/stat-dto/src/main/java/ru/practicum/ViewStatsDTO.java b/stat/stat-dto/src/main/java/ru/practicum/ViewStatsDTO.java new file mode 100644 index 0000000..3c5b787 --- /dev/null +++ b/stat/stat-dto/src/main/java/ru/practicum/ViewStatsDTO.java @@ -0,0 +1,19 @@ +package ru.practicum; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ViewStatsDTO { + + @NotBlank + private String app; + + @NotBlank + private String uri; + private Integer hits; +} diff --git a/stat/stat-server/Dockerfile b/stat/stat-server/Dockerfile new file mode 100644 index 0000000..0ff1817 --- /dev/null +++ b/stat/stat-server/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:21-jre-jammy +VOLUME /tmp +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"] \ No newline at end of file diff --git a/stat/stat-server/pom.xml b/stat/stat-server/pom.xml new file mode 100644 index 0000000..9b85989 --- /dev/null +++ b/stat/stat-server/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + ru.practicum + stat + 0.0.1-SNAPSHOT + + + stat-server + + + 1.6.3 + 0.2.0 + + + + + ru.practicum + stat-dto + 0.0.1-SNAPSHOT + compile + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + org.postgresql + postgresql + runtime + + + + com.h2database + h2 + runtime + + + + org.mapstruct + mapstruct + 1.6.3 + + + + org.mapstruct + mapstruct-processor + 1.6.3 + provided + + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + true + + + -Amapstruct.suppressGeneratorTimestamp=true + + + -Amapstruct.suppressGeneratorVersionInfoComment=true + + + -Amapstruct.verbose=true + + + + + + + + + \ No newline at end of file diff --git a/stat/stat-server/src/main/java/ru/practicum/StatServerApp.java b/stat/stat-server/src/main/java/ru/practicum/StatServerApp.java new file mode 100644 index 0000000..600b780 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/StatServerApp.java @@ -0,0 +1,11 @@ +package ru.practicum; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StatServerApp { + public static void main(String[] args) { + SpringApplication.run(StatServerApp.class, args); + } +} \ No newline at end of file diff --git a/stat/stat-server/src/main/java/ru/practicum/controller/ErrorResponse.java b/stat/stat-server/src/main/java/ru/practicum/controller/ErrorResponse.java new file mode 100644 index 0000000..6fb83aa --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/controller/ErrorResponse.java @@ -0,0 +1,13 @@ +package ru.practicum.controller; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public class ErrorResponse { + private HttpStatus status; + private String message; + private String stackTrace; +} diff --git a/stat/stat-server/src/main/java/ru/practicum/controller/StatsErrorHandler.java b/stat/stat-server/src/main/java/ru/practicum/controller/StatsErrorHandler.java new file mode 100644 index 0000000..f258a0b --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/controller/StatsErrorHandler.java @@ -0,0 +1,51 @@ +package ru.practicum.controller; + +import lombok.extern.slf4j.Slf4j; +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 ru.practicum.exceptions.NotFoundException; +import ru.practicum.exceptions.ValidationException; + +import java.io.PrintWriter; +import java.io.StringWriter; + +@RestControllerAdvice +@Slf4j +public class StatsErrorHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException(final RuntimeException e) { + log.error("500 {}", e.getMessage(), e); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + return new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage(), stackTrace); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleValidationException(final ValidationException e) { + log.error("400 {}", e.getMessage(), e); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + return new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage(), stackTrace); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNotFoundException(final NotFoundException e) { + log.error("404 {}", e.getMessage(), e); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + return new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage(), stackTrace); + } + +} diff --git a/stat/stat-server/src/main/java/ru/practicum/controller/StatsServerController.java b/stat/stat-server/src/main/java/ru/practicum/controller/StatsServerController.java new file mode 100644 index 0000000..4ac455b --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/controller/StatsServerController.java @@ -0,0 +1,29 @@ +package ru.practicum.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.EndpointHitDTO; +import ru.practicum.StatsRequestDTO; +import ru.practicum.ViewStatsDTO; +import ru.practicum.service.StatsServiceImpl; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class StatsServerController { + private final StatsServiceImpl statsService; + + @GetMapping("/stats") + private List viewStats(@ModelAttribute @Valid StatsRequestDTO statsRequestDTO) { + return statsService.getStats(statsRequestDTO); + } + + @PostMapping("/hit") + @ResponseStatus(HttpStatus.CREATED) + private void createHit(@RequestBody @Valid EndpointHitDTO hitDTO) { + statsService.createHit(hitDTO); + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/exceptions/NotFoundException.java b/stat/stat-server/src/main/java/ru/practicum/exceptions/NotFoundException.java new file mode 100644 index 0000000..4f82b7d --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/exceptions/NotFoundException.java @@ -0,0 +1,12 @@ +package ru.practicum.exceptions; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException() { + super(); + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/exceptions/ValidationException.java b/stat/stat-server/src/main/java/ru/practicum/exceptions/ValidationException.java new file mode 100644 index 0000000..3fd492f --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/exceptions/ValidationException.java @@ -0,0 +1,12 @@ +package ru.practicum.exceptions; + +public class ValidationException extends RuntimeException { + + public ValidationException() { + super(); + } + + public ValidationException(String message) { + super(message); + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/mapper/HitsMapper.java b/stat/stat-server/src/main/java/ru/practicum/mapper/HitsMapper.java new file mode 100644 index 0000000..b9baff5 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/mapper/HitsMapper.java @@ -0,0 +1,12 @@ +package ru.practicum.mapper; + +import org.mapstruct.Mapper; +import ru.practicum.EndpointHitDTO; +import ru.practicum.model.EndpointHit; + +@Mapper(componentModel = "spring", uses = {EndpointHit.class}) +public interface HitsMapper { + + EndpointHit mapHitDTO(EndpointHitDTO endpointHitDTO); + +} diff --git a/stat/stat-server/src/main/java/ru/practicum/mapper/HitsObjectMapper.java b/stat/stat-server/src/main/java/ru/practicum/mapper/HitsObjectMapper.java new file mode 100644 index 0000000..1077ec0 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/mapper/HitsObjectMapper.java @@ -0,0 +1,23 @@ +package ru.practicum.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.practicum.model.EndpointHit; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class HitsObjectMapper implements RowMapper { + @Override + public EndpointHit mapRow(ResultSet rs, int rowNum) throws SQLException { + EndpointHit endpointHit = new EndpointHit(); + endpointHit.setApp(rs.getString("app")); + endpointHit.setId(rs.getLong("id")); + endpointHit.setIp(rs.getString("ip")); + endpointHit.setTimestamp(rs.getTimestamp("timestamp").toLocalDateTime()); + endpointHit.setUri(rs.getString("uri")); + + return endpointHit; + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsMapper.java b/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsMapper.java new file mode 100644 index 0000000..aa90029 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsMapper.java @@ -0,0 +1,13 @@ +package ru.practicum.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import ru.practicum.ViewStatsDTO; +import ru.practicum.model.ViewStats; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface ViewStatsMapper { + ViewStats mapViewStatsDTO(ViewStatsDTO viewStatsDTO); + + ViewStatsDTO mapViewStats(ViewStats viewStats); +} diff --git a/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsObjectMapper.java b/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsObjectMapper.java new file mode 100644 index 0000000..02a1690 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/mapper/ViewStatsObjectMapper.java @@ -0,0 +1,19 @@ +package ru.practicum.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.practicum.model.ViewStats; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class ViewStatsObjectMapper implements RowMapper { + @Override + public ViewStats mapRow(ResultSet rs, int rowNum) throws SQLException { + ViewStats viewStats = new ViewStats(); + viewStats.setApp(rs.getString("app")); + viewStats.setUri(rs.getString("uri")); + return viewStats; + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/model/EndpointHit.java b/stat/stat-server/src/main/java/ru/practicum/model/EndpointHit.java new file mode 100644 index 0000000..dfaecf2 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/model/EndpointHit.java @@ -0,0 +1,18 @@ +package ru.practicum.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EndpointHit { + private Long id; + private String app; + private String uri; + private String ip; + private LocalDateTime timestamp; +} diff --git a/stat/stat-server/src/main/java/ru/practicum/model/ViewStats.java b/stat/stat-server/src/main/java/ru/practicum/model/ViewStats.java new file mode 100644 index 0000000..9707b8f --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/model/ViewStats.java @@ -0,0 +1,14 @@ +package ru.practicum.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ViewStats { + private String app; + private String uri; + private Integer hits; +} diff --git a/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepoInterface.java b/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepoInterface.java new file mode 100644 index 0000000..6d382d8 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepoInterface.java @@ -0,0 +1,15 @@ +package ru.practicum.repository; + +import ru.practicum.model.EndpointHit; +import ru.practicum.model.ViewStats; + +import java.time.LocalDateTime; +import java.util.List; + +public interface StatsServerRepoInterface { + List getStats(LocalDateTime start, LocalDateTime end); + + List getStatsUnique(LocalDateTime start, LocalDateTime end, List uris); + + void createHit(EndpointHit hit); +} diff --git a/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepository.java b/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepository.java new file mode 100644 index 0000000..228d1b9 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/repository/StatsServerRepository.java @@ -0,0 +1,54 @@ +package ru.practicum.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.practicum.mapper.ViewStatsObjectMapper; +import ru.practicum.model.EndpointHit; +import ru.practicum.model.ViewStats; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class StatsServerRepository extends ViewStatsObjectMapper implements StatsServerRepoInterface { + + private final JdbcTemplate jdbc; + private final ViewStatsObjectMapper viewStatsMapper = new ViewStatsObjectMapper(); + + @Override + public List getStats(LocalDateTime start, LocalDateTime end) { + List viewStatsList = jdbc.query("SELECT app, uri FROM hits " + + "WHERE timestamp BETWEEN ? AND ? GROUP BY hits.app, hits.uri " + + "ORDER BY COUNT(*) DESC", new ViewStatsObjectMapper(), start, end); + + for (ViewStats viewStats : viewStatsList) { + int hits = jdbc.queryForObject("SELECT COUNT(uri) FROM hits " + + "WHERE uri = ?", Integer.class, viewStats.getUri()); + viewStats.setHits(hits); + } + return viewStatsList; + } + + @Override + public List getStatsUnique(LocalDateTime start, LocalDateTime end, List uris) { + List viewStatsList = jdbc.query("SELECT app, uri FROM hits " + + "WHERE uri = ? AND timestamp BETWEEN ? AND ? GROUP BY hits.app, hits.uri " + + "ORDER BY COUNT(*) DESC", new ViewStatsObjectMapper(), uris.getFirst(), start, end); + + for (ViewStats viewStats : viewStatsList) { + int hits = jdbc.queryForObject("SELECT COUNT(DISTINCT ip) FROM hits " + + "WHERE uri = ?", Integer.class, viewStats.getUri()); + viewStats.setHits(hits); + } + return viewStatsList; + } + + @Override + public void createHit(EndpointHit hit) { + jdbc.update("INSERT INTO hits (app, uri, ip, timestamp) VALUES (?, ?, ?, ?)", + hit.getApp(), hit.getUri(), hit.getIp(), hit.getTimestamp()); + } + +} diff --git a/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceImpl.java b/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceImpl.java new file mode 100644 index 0000000..c824a8c --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceImpl.java @@ -0,0 +1,53 @@ +package ru.practicum.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.EndpointHitDTO; +import ru.practicum.StatsRequestDTO; +import ru.practicum.ViewStatsDTO; +import ru.practicum.exceptions.ValidationException; +import ru.practicum.mapper.HitsMapper; +import ru.practicum.mapper.ViewStatsMapper; +import ru.practicum.model.EndpointHit; +import ru.practicum.repository.StatsServerRepository; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StatsServiceImpl implements StatsServiceInterface { + private final StatsServerRepository statsService; + private final ViewStatsMapper viewStatsMapper; + private final HitsMapper hitsMapper; + + @Override + public List getStats(StatsRequestDTO statsRequestDTO) { + if (statsRequestDTO.getStart().isAfter(statsRequestDTO.getEnd())) { + throw new ValidationException("Start date cannot be after end date"); + } + + List stats = new ArrayList<>(); + if (statsRequestDTO.getUnique() != null && statsRequestDTO.getUnique()) { + stats = statsService.getStatsUnique(statsRequestDTO.getStart(), statsRequestDTO.getEnd(), + statsRequestDTO.getUris()).stream() + .map(viewStatsMapper::mapViewStats) + .toList(); + } + + if (statsRequestDTO.getUnique() == null || statsRequestDTO.getUnique().equals(false)) { + stats = statsService.getStats(statsRequestDTO.getStart(), statsRequestDTO.getEnd()).stream() + .filter(viewStats -> statsRequestDTO.getUris().contains(viewStats.getUri())) + .map(viewStatsMapper::mapViewStats) + .toList(); + + } + return stats; + } + + @Override + public void createHit(EndpointHitDTO hitDTO) { + EndpointHit hit = hitsMapper.mapHitDTO(hitDTO); + statsService.createHit(hit); + } +} diff --git a/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceInterface.java b/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceInterface.java new file mode 100644 index 0000000..0263295 --- /dev/null +++ b/stat/stat-server/src/main/java/ru/practicum/service/StatsServiceInterface.java @@ -0,0 +1,13 @@ +package ru.practicum.service; + +import ru.practicum.EndpointHitDTO; +import ru.practicum.StatsRequestDTO; +import ru.practicum.ViewStatsDTO; + +import java.util.List; + +public interface StatsServiceInterface { + List getStats(StatsRequestDTO statsRequestDTO); + + void createHit(EndpointHitDTO hitDTO); +} diff --git a/stat/stat-server/src/main/resources/application-docker.yml b/stat/stat-server/src/main/resources/application-docker.yml new file mode 100644 index 0000000..fd0caa7 --- /dev/null +++ b/stat/stat-server/src/main/resources/application-docker.yml @@ -0,0 +1,18 @@ +server: + port: 9090 + +spring: + main: + allow-bean-definition-overriding: true + jpa: + hibernate: + ddl-auto: none + properties.hibernate.format_sql: true + sql: + init: + mode: always + datasource: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://stats-server:5432/stats-server + username: user + password: 12345 \ No newline at end of file diff --git a/stat/stat-server/src/main/resources/application.yml b/stat/stat-server/src/main/resources/application.yml new file mode 100644 index 0000000..103c584 --- /dev/null +++ b/stat/stat-server/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + port: 9090 + +spring: + main: + allow-bean-definition-overriding: true + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + jdbc: + lob: + non_contextual_creation: true + show-sql: true + sql: + init: + mode: always + datasource: + driverClassName: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/stats-server + username: user + password: 12345 + + diff --git a/stat/stat-server/src/main/resources/schema.sql b/stat/stat-server/src/main/resources/schema.sql new file mode 100644 index 0000000..fb8f050 --- /dev/null +++ b/stat/stat-server/src/main/resources/schema.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS hits; + +CREATE TABLE IF NOT EXISTS hits ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + app VARCHAR (255) NOT NULL, + uri VARCHAR (100) NOT NULL, + ip VARCHAR (50) NOT NULL, + timestamp TIMESTAMP WITHOUT TIME ZONE +);