diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java b/rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java index d1daf87..bf8039a 100644 --- a/rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/configs/SecurityConfig.java @@ -70,6 +70,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorize -> authorize .requestMatchers(PUBLIC_URLS).permitAll() .requestMatchers(HttpMethod.GET, PUBLIC_URLS_GET).permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/properties/filtered/").permitAll() .requestMatchers(HttpMethod.DELETE, ADMIN_URLS).hasAuthority("ROLE_ADMIN") .requestMatchers(HttpMethod.POST, ADMIN_URLS).hasAuthority("ROLE_ADMIN") .requestMatchers(HttpMethod.PATCH, ADMIN_URLS).hasAuthority("ROLE_ADMIN") diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/controllers/PropertyController.java b/rentplace/src/main/java/kattsyn/dev/rentplace/controllers/PropertyController.java index f2e9ebc..aef4fed 100644 --- a/rentplace/src/main/java/kattsyn/dev/rentplace/controllers/PropertyController.java +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/controllers/PropertyController.java @@ -12,6 +12,7 @@ import kattsyn.dev.rentplace.dtos.ImageDTO; import kattsyn.dev.rentplace.dtos.PropertyCreateEditDTO; import kattsyn.dev.rentplace.dtos.PropertyDTO; +import kattsyn.dev.rentplace.dtos.filters.PropertyFilterDTO; import kattsyn.dev.rentplace.services.PropertyService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -76,6 +77,20 @@ public ResponseEntity> getProperties() { return ResponseEntity.ok(properties); } + @Operation( + summary = "Получение всех объявлений, с фильтрацией", + description = "Позволяет получить все объявления, с фильтрацией" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Успешно", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PropertyDTO[].class))), + @ApiResponse(responseCode = "500", description = "Непредвиденная ошибка со стороны сервера", content = @Content) + }) + @PostMapping(path = "/filtered/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> getPropertiesByFilter(@Valid @ModelAttribute PropertyFilterDTO propertyFilter) { + List properties = propertyService.findAllByFilter(propertyFilter); + return ResponseEntity.ok(properties); + } + @Operation( summary = "Получение объявлений пользователя", description = "Позволяет получить все объявления пользователя по его токену. Только для авторизованных пользователей." diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/dtos/filters/PropertyFilterDTO.java b/rentplace/src/main/java/kattsyn/dev/rentplace/dtos/filters/PropertyFilterDTO.java new file mode 100644 index 0000000..5b7c04d --- /dev/null +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/dtos/filters/PropertyFilterDTO.java @@ -0,0 +1,35 @@ +package kattsyn.dev.rentplace.dtos.filters; + +import jakarta.validation.constraints.Min; +import kattsyn.dev.rentplace.enums.SortType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PropertyFilterDTO { + + private SortType sortType; + private List categoryIds; + private List facilityIds; + private Boolean isLongTermRent; + @Min(0) + private Integer minPrice; + @Min(0) + private Integer maxPrice; + @Min(0) + private Integer guestsAmount; + @Min(0) + private Integer bedsAmount; + @Min(0) + private Integer bedrooms; + @Min(0) + private Integer rooms; + +} diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/enums/SortType.java b/rentplace/src/main/java/kattsyn/dev/rentplace/enums/SortType.java new file mode 100644 index 0000000..1565899 --- /dev/null +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/enums/SortType.java @@ -0,0 +1,19 @@ +package kattsyn.dev.rentplace.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + description = "Тип сортировки объявлений" +) +public enum SortType { + + @Schema(name = "Сначала новые") + MOST_RECENT, + @Schema(name = "Сначала старые") + MOST_OLD, + @Schema(name = "Сначала дорогие, убывание по цене") + MOST_EXPENSIVE, + @Schema(name = "Сначала дешевые, возрастание по цене") + MOST_CHEAP + +} diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/repositories/PropertyRepository.java b/rentplace/src/main/java/kattsyn/dev/rentplace/repositories/PropertyRepository.java index cc62187..12c7010 100644 --- a/rentplace/src/main/java/kattsyn/dev/rentplace/repositories/PropertyRepository.java +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/repositories/PropertyRepository.java @@ -3,13 +3,14 @@ import kattsyn.dev.rentplace.entities.Property; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; -public interface PropertyRepository extends JpaRepository { +public interface PropertyRepository extends JpaRepository, JpaSpecificationExecutor { @Query(""" SELECT DISTINCT p diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/services/PropertyService.java b/rentplace/src/main/java/kattsyn/dev/rentplace/services/PropertyService.java index 545be3f..b6e169d 100644 --- a/rentplace/src/main/java/kattsyn/dev/rentplace/services/PropertyService.java +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/services/PropertyService.java @@ -3,6 +3,7 @@ import kattsyn.dev.rentplace.dtos.ImageDTO; import kattsyn.dev.rentplace.dtos.PropertyCreateEditDTO; import kattsyn.dev.rentplace.dtos.PropertyDTO; +import kattsyn.dev.rentplace.dtos.filters.PropertyFilterDTO; import kattsyn.dev.rentplace.entities.Property; import org.springframework.web.multipart.MultipartFile; @@ -18,6 +19,8 @@ public interface PropertyService { List findAllByOwnerEmail(String email); + List findAllByFilter(PropertyFilterDTO filter); + PropertyDTO findById(long id); Property getPropertyById(long id); diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/services/impl/PropertyServiceImpl.java b/rentplace/src/main/java/kattsyn/dev/rentplace/services/impl/PropertyServiceImpl.java index 82daa9e..54523ad 100644 --- a/rentplace/src/main/java/kattsyn/dev/rentplace/services/impl/PropertyServiceImpl.java +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/services/impl/PropertyServiceImpl.java @@ -4,11 +4,13 @@ import kattsyn.dev.rentplace.dtos.ImageDTO; import kattsyn.dev.rentplace.dtos.PropertyCreateEditDTO; import kattsyn.dev.rentplace.dtos.PropertyDTO; +import kattsyn.dev.rentplace.dtos.filters.PropertyFilterDTO; import kattsyn.dev.rentplace.entities.Image; import kattsyn.dev.rentplace.entities.Property; import kattsyn.dev.rentplace.entities.User; import kattsyn.dev.rentplace.enums.ImageType; import kattsyn.dev.rentplace.enums.Role; +import kattsyn.dev.rentplace.enums.SortType; import kattsyn.dev.rentplace.exceptions.ForbiddenException; import kattsyn.dev.rentplace.exceptions.NotFoundException; import kattsyn.dev.rentplace.mappers.ImageMapper; @@ -17,8 +19,10 @@ import kattsyn.dev.rentplace.services.ImageService; import kattsyn.dev.rentplace.services.PropertyService; import kattsyn.dev.rentplace.services.UserService; +import kattsyn.dev.rentplace.specifications.PropertySpecification; import kattsyn.dev.rentplace.utils.PathResolver; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -69,6 +73,24 @@ public List findAllByOwnerEmail(String email) { return propertyMapper.fromProperties(propertyRepository.findAllByOwnerEmail(email)); } + @Override + public List findAllByFilter(PropertyFilterDTO filter) { + return propertyMapper.fromProperties( + propertyRepository.findAll(new PropertySpecification(filter), buildSort(filter.getSortType())) + ); + } + + private Sort buildSort(SortType sortType) { + if (sortType == null) return Sort.unsorted(); + + return switch (sortType) { + case MOST_OLD -> Sort.by(Sort.Order.asc("propertyId")); + case MOST_RECENT -> Sort.by(Sort.Order.desc("propertyId")); + case MOST_EXPENSIVE -> Sort.by(Sort.Order.desc("cost")); + case MOST_CHEAP -> Sort.by(Sort.Order.asc("cost")); + }; + } + @Override @Transactional public Property getPropertyById(long id) { diff --git a/rentplace/src/main/java/kattsyn/dev/rentplace/specifications/PropertySpecification.java b/rentplace/src/main/java/kattsyn/dev/rentplace/specifications/PropertySpecification.java new file mode 100644 index 0000000..6e3f206 --- /dev/null +++ b/rentplace/src/main/java/kattsyn/dev/rentplace/specifications/PropertySpecification.java @@ -0,0 +1,91 @@ +package kattsyn.dev.rentplace.specifications; + +import jakarta.persistence.criteria.*; +import kattsyn.dev.rentplace.dtos.filters.PropertyFilterDTO; +import kattsyn.dev.rentplace.entities.Category; +import kattsyn.dev.rentplace.entities.Facility; +import kattsyn.dev.rentplace.entities.Property; +import lombok.AllArgsConstructor; +import org.springframework.data.jpa.domain.Specification; + +@AllArgsConstructor +public class PropertySpecification implements Specification { + + private final PropertyFilterDTO filter; + + @Override + public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { + + Predicate predicate = criteriaBuilder.conjunction(); + + root.fetch("images", JoinType.LEFT); + root.fetch("facilities", JoinType.LEFT); + root.fetch("categories", JoinType.LEFT); + + assert query != null; + query.distinct(true); + + Join categoryJoin = null; + if (filter.getCategoryIds() != null && !filter.getCategoryIds().isEmpty()) { + categoryJoin = root.join("categories", JoinType.INNER); + } + + Join facilityJoin = null; + if (filter.getFacilityIds() != null && !filter.getFacilityIds().isEmpty()) { + facilityJoin = root.join("facilities", JoinType.INNER); + } + + if (filter.getMinPrice() != null && filter.getMinPrice() >= 0) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get("cost"), filter.getMinPrice())); + } + + if (filter.getMaxPrice() != null && filter.getMaxPrice() >= 0) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder.lessThanOrEqualTo(root.get("cost"), filter.getMaxPrice())); + } + + if (filter.getGuestsAmount() != null && !(filter.getGuestsAmount() == 0)) { + predicate = filter.getGuestsAmount() >= 5 + ? criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get("maxGuests"), filter.getGuestsAmount())) + : criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get("maxGuests"), filter.getGuestsAmount())); + } + + if (filter.getBedsAmount() != null && !(filter.getBedsAmount() == 0)) { + predicate = filter.getBedsAmount() >= 5 + ? criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get("sleepingPlaces"), filter.getBedsAmount())) + : criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get("sleepingPlaces"), filter.getBedsAmount())); + } + + if (filter.getBedrooms() != null && !(filter.getBedrooms() == 0)) { + predicate = filter.getBedrooms() >= 5 + ? criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get("bedrooms"), filter.getBedrooms())) + : criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get("bedrooms"), filter.getBedrooms())); + } + + if (filter.getRooms() != null && !(filter.getRooms() == 0)) { + predicate = filter.getRooms() >= 5 + ? criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get("rooms"), filter.getRooms())) + : criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get("rooms"), filter.getRooms())); + } + + if (filter.getIsLongTermRent() != null) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get("isLongTermRent"), filter.getIsLongTermRent())); + } + + if (filter.getCategoryIds() != null && !filter.getCategoryIds().isEmpty() && categoryJoin != null) { + predicate = criteriaBuilder.and( + predicate, + categoryJoin.get("categoryId").in(filter.getCategoryIds()) + ); + } + + // Фильтр по удобствам + if (filter.getFacilityIds() != null && !filter.getFacilityIds().isEmpty() && facilityJoin != null) { + predicate = criteriaBuilder.and( + predicate, + facilityJoin.get("facilityId").in(filter.getFacilityIds()) + ); + } + + return predicate; + } +}