diff --git a/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilter.java b/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilter.java new file mode 100644 index 0000000..176d1ac --- /dev/null +++ b/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilter.java @@ -0,0 +1,75 @@ +package de.ph87.kleinanzeigen.crud; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import lombok.Data; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +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 java.util.Collections; +import java.util.List; + +@Getter +@ToString +public class CrudFilter { + + protected final int page; + + protected final int size; + + @NonNull + protected final List orders; + + public CrudFilter(final int page, final int size, @NonNull final List orders) { + this.page = page; + this.size = size; + this.orders = orders; + } + + @NonNull + public Pageable getPageable() { + return PageRequest.of(page, size, getSort()); + } + + @NonNull + private Sort getSort() { + return Sort.by(orders.stream().map(Order::toOrder).toList()); + } + + @NonNull + public Specification getSpecification() { + return (root, query, criteriaBuilder) -> { + final List predicates = getPredicates(root, query, criteriaBuilder); + return criteriaBuilder.and(predicates.toArray(Predicate[]::new)); + }; + } + + @NonNull + protected List getPredicates(@NonNull final Root root, final CriteriaQuery query, @NonNull final CriteriaBuilder criteriaBuilder) { + return Collections.emptyList(); + } + + @Data + public static class Order { + + @NonNull + private final String property; + + @NonNull + private final Sort.Direction direction; + + @NonNull + public Sort.Order toOrder() { + return new Sort.Order(direction, property); + } + + } + +} diff --git a/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilterSearch.java b/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilterSearch.java new file mode 100644 index 0000000..ca6afc1 --- /dev/null +++ b/src/main/java/de/ph87/kleinanzeigen/crud/CrudFilterSearch.java @@ -0,0 +1,57 @@ +package de.ph87.kleinanzeigen.crud; + +import jakarta.persistence.criteria.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +@Getter +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public abstract class CrudFilterSearch extends CrudFilter { + + @NonNull + protected final String search; + + protected CrudFilterSearch(final int page, final int size, @NonNull final List orders, @NonNull final String search) { + super(page, size, orders); + this.search = search; + } + + @NonNull + protected List getPredicates(@NonNull final Root root, final CriteriaQuery query, @NonNull final CriteriaBuilder criteriaBuilder) { + return new ArrayList<>(search(root, criteriaBuilder)); + } + + @NonNull + public List search(@NonNull final Root root, @NonNull final CriteriaBuilder criteriaBuilder) { + final String[] words = search.replaceAll("([0-9])([a-zA-Z])", "$1 $2") + .replaceAll("([a-zA-Z])([0-9])", "$1 $2") + .replaceAll("([a-z])([A-Z])", "$1 $2") + .replaceAll("^\\W+|\\W+$", "") + .toLowerCase(Locale.ROOT) + .split("\\W+"); + final List> fields = getFields(root); + return predicateAllWordsEachInAnyField(criteriaBuilder, words, fields); + } + + @NonNull + private List predicateAllWordsEachInAnyField(@NonNull final CriteriaBuilder criteriaBuilder, @NonNull final String[] words, @NonNull final List> fields) { + return Arrays.stream(words).map(word -> predicateWordInAnyField(criteriaBuilder, word, fields)).toList(); + } + + @NonNull + private Predicate predicateWordInAnyField(@NonNull final CriteriaBuilder criteriaBuilder, @NonNull final String word, @NonNull final List> fields) { + return fields.stream().map(field -> criteriaBuilder.like(field, "%" + word + "%")).reduce(criteriaBuilder::or).orElseThrow(); + } + + @NonNull + protected abstract List> getFields(@NonNull final Root root); + +} diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferController.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferController.java new file mode 100644 index 0000000..13e3959 --- /dev/null +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferController.java @@ -0,0 +1,24 @@ +package de.ph87.kleinanzeigen.kleinanzeigen.offer; + +import jakarta.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +@CrossOrigin +@RestController +@RequiredArgsConstructor +@RequestMapping("Offer") +public class OfferController { + + private final OfferService offerService; + + @GetMapping("filter") + public Page getAllOffers(@Nullable @RequestBody(required = false) final OfferFilter filter) { + if (filter == null) { + return offerService.filter(OfferFilter.DEFAULT()); + } + return offerService.filter(filter); + } + +} diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferFilter.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferFilter.java new file mode 100644 index 0000000..615fa84 --- /dev/null +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferFilter.java @@ -0,0 +1,30 @@ +package de.ph87.kleinanzeigen.kleinanzeigen.offer; + +import de.ph87.kleinanzeigen.crud.CrudFilterSearch; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import org.springframework.data.domain.Sort; + +import java.util.List; + +@Getter +@ToString(callSuper = true) +public class OfferFilter extends CrudFilterSearch { + + public OfferFilter(final int page, final int size, @NonNull final List orders, @NonNull final String search) { + super(page, size, orders, search); + } + + @Override + protected @NonNull List> getFields(final @NonNull Root root) { + return List.of(root.get("title"), root.get("description")); + } + + public static OfferFilter DEFAULT() { + return new OfferFilter(0, 30, List.of(new Order("articleDate", Sort.Direction.DESC)), ""); + } + +} diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferRepository.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferRepository.java index 69c70d0..2fb3e80 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferRepository.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferRepository.java @@ -1,10 +1,11 @@ package de.ph87.kleinanzeigen.kleinanzeigen.offer; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.ListCrudRepository; import java.util.Optional; -public interface OfferRepository extends ListCrudRepository { +public interface OfferRepository extends ListCrudRepository, JpaSpecificationExecutor { Optional findByArticleId(String articleId); diff --git a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java index 9bea355..2d15fa8 100644 --- a/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java +++ b/src/main/java/de/ph87/kleinanzeigen/kleinanzeigen/offer/OfferService.java @@ -4,6 +4,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,19 +19,16 @@ public class OfferService { private final ApplicationEventPublisher publisher; public void updateOrCreate(@NonNull final OfferCreate create, final boolean resendOnPriceChange) { - offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse( - existing -> { - if (existing.update(create, resendOnPriceChange)) { - final OfferDto dto = toDto(existing); - publisher.publishEvent(dto); - } - }, - () -> { - final Offer offer = offerRepository.save(new Offer(create, resendOnPriceChange)); - final OfferDto dto = toDto(offer); + offerRepository.findByArticleId(create.getArticleId()).ifPresentOrElse(existing -> { + if (existing.update(create, resendOnPriceChange)) { + final OfferDto dto = toDto(existing); publisher.publishEvent(dto); } - ); + }, () -> { + final Offer offer = offerRepository.save(new Offer(create, resendOnPriceChange)); + final OfferDto dto = toDto(offer); + publisher.publishEvent(dto); + }); } public OfferDto toDto(final Offer offer) { @@ -41,4 +39,9 @@ public class OfferService { return offerRepository.findById(offerDto.getId()).orElseThrow(); } + @NonNull + public Page filter(@NonNull final OfferFilter filter) { + return offerRepository.findAll(filter.getSpecification(), filter.getPageable()).map(OfferDto::new); + } + }