From 6e2b86153e831cf3745e2a4b4266b96590c5d0e2 Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Fri, 15 Mar 2024 14:56:34 +0100 Subject: [PATCH] Add search publications use case but it's bugged. --- codiki-application/pom.xml | 5 + .../PublicationSearchCriteriaFactory.java | 132 +++++++++++++++++ .../publication/PublicationUseCases.java | 14 +- .../PublicationSearchCriteriaFactoryTest.java | 137 ++++++++++++++++++ .../model/search/ComparisonType.java | 8 + .../search/PublicationSearchCriterion.java | 7 + .../model/search/PublicationSearchField.java | 26 ++++ .../publication/port/PublicationPort.java | 4 + .../security/SecurityConfiguration.java | 1 + .../publication/PublicationController.java | 10 ++ codiki-infrastructure/pom.xml | 19 +++ .../publication/PublicationJpaAdapter.java | 18 ++- .../PublicationSearchCriteriaJpaAdapter.java | 35 +++++ .../publication/model/AuthorEntity.java | 4 +- .../CustomPublicationRepository.java | 10 ++ .../CustomPublicationRepositoryImpl.java | 41 ++++++ .../PublicationPredicateMapper.java | 79 ++++++++++ .../repository/PublicationRepository.java | 2 +- ...blicationSearchCriteriaJpaAdapterTest.java | 40 +++++ .../src/main/resources/application-local.yml | 2 + .../Codiki/environments/localhost.bru | 2 +- 21 files changed, 589 insertions(+), 7 deletions(-) create mode 100644 codiki-application/src/main/java/org/codiki/application/publication/PublicationSearchCriteriaFactory.java create mode 100644 codiki-application/src/test/java/org/codiki/application/publication/PublicationSearchCriteriaFactoryTest.java create mode 100644 codiki-domain/src/main/java/org/codiki/domain/publication/model/search/ComparisonType.java create mode 100644 codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchCriterion.java create mode 100644 codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchField.java create mode 100644 codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapter.java create mode 100644 codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepository.java create mode 100644 codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepositoryImpl.java create mode 100644 codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationPredicateMapper.java create mode 100644 codiki-infrastructure/src/test/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapterTest.java diff --git a/codiki-application/pom.xml b/codiki-application/pom.xml index 2f6f156..0fbaf05 100644 --- a/codiki-application/pom.xml +++ b/codiki-application/pom.xml @@ -38,6 +38,11 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + org.assertj assertj-core diff --git a/codiki-application/src/main/java/org/codiki/application/publication/PublicationSearchCriteriaFactory.java b/codiki-application/src/main/java/org/codiki/application/publication/PublicationSearchCriteriaFactory.java new file mode 100644 index 0000000..7dd15a5 --- /dev/null +++ b/codiki-application/src/main/java/org/codiki/application/publication/PublicationSearchCriteriaFactory.java @@ -0,0 +1,132 @@ +package org.codiki.application.publication; + +import static java.util.stream.Collectors.toSet; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS; +import static org.codiki.domain.publication.model.search.ComparisonType.EQUALS; +import static org.codiki.domain.publication.model.search.PublicationSearchField.AUTHOR_ID; +import static org.codiki.domain.publication.model.search.PublicationSearchField.AUTHOR_PSEUDO; +import static org.codiki.domain.publication.model.search.PublicationSearchField.CATEGORY_ID; +import static org.codiki.domain.publication.model.search.PublicationSearchField.DESCRIPTION; +import static org.codiki.domain.publication.model.search.PublicationSearchField.ID; +import static org.codiki.domain.publication.model.search.PublicationSearchField.KEY; +import static org.codiki.domain.publication.model.search.PublicationSearchField.TEXT; +import static org.codiki.domain.publication.model.search.PublicationSearchField.TITLE; +import static org.springframework.util.ObjectUtils.isEmpty; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.codiki.domain.publication.model.search.PublicationSearchField; +import org.springframework.stereotype.Component; + +@Component +public class PublicationSearchCriteriaFactory { + private static final Pattern ACCENT_LETTER_REGEX = Pattern.compile("[à-ü]|[À-Ü]"); + private static final List ID_SEARCH_FIELDS = List.of(ID, CATEGORY_ID, AUTHOR_ID); + + public List buildCriteria(String searchQuery) { + Set stringCriteria = Set.of(searchQuery.split(" ")); + + return stringCriteria.stream() + .map(this::buildPublicationSearchCriterion) + .flatMap(List::stream) + .toList(); + } + + private List buildPublicationSearchCriterion(String criterion) { + List result; + + if (criterion.contains("=")) { + String[] criterionParts = criterion.split("="); + + if (criterionParts.length > 2) { + result = buildDefaultContainsCriteria(criterion); + } else { + String criterionSearchFieldAsString = criterionParts[0]; + String criterionValue = criterionParts[1]; + + result = PublicationSearchField.from(criterionSearchFieldAsString) + .map(searchField -> { + List criteria; + if (ID_SEARCH_FIELDS.contains(searchField)) { + criteria = convertToUuid(criterionValue) + .map(uuidCriterion -> new PublicationSearchCriterion(searchField, EQUALS, uuidCriterion)) + .map(List::of) + .orElse(buildDefaultContainsCriteria(criterion)); + } else { + criteria = List.of(new PublicationSearchCriterion(searchField, EQUALS, criterionValue)); + } + return criteria; + }) + .orElse(buildDefaultContainsCriteria(criterion)); + } + } else { + result = buildDefaultContainsCriteria(criterion); + } + + return result; + } + + private Optional convertToUuid(String uuidValue) { + Optional result; + try { + result = Optional.of(UUID.fromString(uuidValue)); + } catch (IllegalArgumentException exception) { + result = Optional.empty(); + } + return result; + } + + private List buildDefaultContainsCriteria(String criterion) { + return List.of( + new PublicationSearchCriterion(KEY, CONTAINS, criterion), + new PublicationSearchCriterion(TITLE, CONTAINS, criterion), + new PublicationSearchCriterion(TEXT, CONTAINS, criterion), + new PublicationSearchCriterion(DESCRIPTION, CONTAINS, criterion), + new PublicationSearchCriterion(AUTHOR_PSEUDO, CONTAINS, criterion) + ); + } + + Set splitAndSanitizeSearchCriterion(String searchQuery) { + Set result = new HashSet<>(); + + for (String fragment : searchQuery.split(" ")) { + Set subFragmentsFromAccentedCharactersSplitting = splitSubFragmentByAccentedCharacters(fragment); + + if (isEmpty(subFragmentsFromAccentedCharactersSplitting)) { + result.add(fragment); + } else { + result.addAll(subFragmentsFromAccentedCharactersSplitting); + } + } + + return result; + } + + private Set splitSubFragmentByAccentedCharacters(String fragment) { + Set result = new HashSet<>(); + + Matcher accentsMatcher = ACCENT_LETTER_REGEX.matcher(fragment); + Set accentedCharacters = new HashSet<>(); + while (accentsMatcher.find()) { + accentedCharacters.add(accentsMatcher.group()); + } + + if (!isEmpty(accentedCharacters)) { + String joinedAccentedCharacters = String.join("", accentedCharacters); + String[] subFragments = fragment.split(String.format("[%s]", joinedAccentedCharacters)); + + result = Stream.of(subFragments) + .filter(subFragment -> subFragment.length() > 1) + .collect(toSet()); + } + + return result; + } +} diff --git a/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java b/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java index 106fedc..22a8ec0 100644 --- a/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java +++ b/codiki-application/src/main/java/org/codiki/application/publication/PublicationUseCases.java @@ -3,6 +3,7 @@ package org.codiki.application.publication; import static java.util.Objects.isNull; import java.time.Clock; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -12,7 +13,6 @@ import org.codiki.application.category.CategoryUseCases; import org.codiki.application.picture.PictureUseCases; import org.codiki.application.user.UserUseCases; import org.codiki.domain.category.exception.CategoryNotFoundException; -import org.codiki.domain.category.model.Category; import org.codiki.domain.exception.AuthenticationRequiredException; import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.publication.exception.PublicationEditionException; @@ -21,6 +21,7 @@ import org.codiki.domain.publication.exception.PublicationUpdateForbiddenExcepti import org.codiki.domain.publication.model.Publication; import org.codiki.domain.publication.model.PublicationEditionRequest; import org.codiki.domain.publication.model.builder.PublicationBuilder; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; import org.codiki.domain.publication.port.PublicationPort; import org.codiki.domain.user.model.User; import org.springframework.stereotype.Service; @@ -31,8 +32,9 @@ public class PublicationUseCases { private final Clock clock; private final KeyGenerator keyGenerator; private final PictureUseCases pictureUseCases; - private final PublicationPort publicationPort; private final PublicationCreationRequestValidator publicationCreationRequestValidator; + private final PublicationPort publicationPort; + private final PublicationSearchCriteriaFactory publicationSearchCriteriaFactory; private final PublicationUpdateRequestValidator publicationUpdateRequestValidator; private final UserUseCases userUseCases; @@ -43,6 +45,7 @@ public class PublicationUseCases { PictureUseCases pictureUseCases, PublicationCreationRequestValidator publicationCreationRequestValidator, PublicationPort publicationPort, + PublicationSearchCriteriaFactory publicationSearchCriteriaFactory, PublicationUpdateRequestValidator publicationUpdateRequestValidator, UserUseCases userUseCases ) { @@ -54,6 +57,7 @@ public class PublicationUseCases { this.publicationUpdateRequestValidator = publicationUpdateRequestValidator; this.userUseCases = userUseCases; this.pictureUseCases = pictureUseCases; + this.publicationSearchCriteriaFactory = publicationSearchCriteriaFactory; } public Publication createPublication(PublicationEditionRequest request) { @@ -161,4 +165,10 @@ public class PublicationUseCases { public Optional findById(UUID publicationId) { return publicationPort.findById(publicationId); } + + public List searchPublications(String searchQuery) { + List criteria = publicationSearchCriteriaFactory.buildCriteria(searchQuery); + + return publicationPort.search(criteria); + } } diff --git a/codiki-application/src/test/java/org/codiki/application/publication/PublicationSearchCriteriaFactoryTest.java b/codiki-application/src/test/java/org/codiki/application/publication/PublicationSearchCriteriaFactoryTest.java new file mode 100644 index 0000000..2a3484f --- /dev/null +++ b/codiki-application/src/test/java/org/codiki/application/publication/PublicationSearchCriteriaFactoryTest.java @@ -0,0 +1,137 @@ +package org.codiki.application.publication; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS; +import static org.codiki.domain.publication.model.search.ComparisonType.EQUALS; +import static org.codiki.domain.publication.model.search.PublicationSearchField.*; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PublicationSearchCriteriaFactoryTest { + private PublicationSearchCriteriaFactory factory; + + @BeforeEach + void setUp() { + factory = new PublicationSearchCriteriaFactory(); + } + + @Nested + public class BuildCriteria { + + @ParameterizedTest + @MethodSource("arguments_of_should_build_criteria_from_search_query") + void should_build_criteria_from_search_query(String searchQuery, List expectedResult) { + // when + List result = factory.buildCriteria(searchQuery); + + // then + assertThat(result).isEqualTo(expectedResult); + } + + private static Stream arguments_of_should_build_criteria_from_search_query() { + return Stream.of( + Arguments.of( + "criterion", + List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "criterion"), + new PublicationSearchCriterion(TITLE, CONTAINS, "criterion"), + new PublicationSearchCriterion(TEXT, CONTAINS, "criterion"), + new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "criterion"), + new PublicationSearchCriterion(AUTHOR_PSEUDO, CONTAINS, "criterion") + ) + ), + Arguments.of( + "key=value=crap", + List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "key=value=crap"), + new PublicationSearchCriterion(TITLE, CONTAINS, "key=value=crap"), + new PublicationSearchCriterion(TEXT, CONTAINS, "key=value=crap"), + new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "key=value=crap"), + new PublicationSearchCriterion(AUTHOR_PSEUDO, CONTAINS, "key=value=crap") + ) + ), + Arguments.of( + "key=abcd", + List.of(new PublicationSearchCriterion(KEY, EQUALS, "abcd")) + ), + Arguments.of( + "crappyFieldName=abcd", + List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "crappyFieldName=abcd"), + new PublicationSearchCriterion(TITLE, CONTAINS, "crappyFieldName=abcd"), + new PublicationSearchCriterion(TEXT, CONTAINS, "crappyFieldName=abcd"), + new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "crappyFieldName=abcd"), + new PublicationSearchCriterion(AUTHOR_PSEUDO, CONTAINS, "crappyFieldName=abcd") + ) + ), + Arguments.of( + "id=abcd", + List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "id=abcd"), + new PublicationSearchCriterion(TITLE, CONTAINS, "id=abcd"), + new PublicationSearchCriterion(TEXT, CONTAINS, "id=abcd"), + new PublicationSearchCriterion(DESCRIPTION, CONTAINS, "id=abcd"), + new PublicationSearchCriterion(AUTHOR_PSEUDO, CONTAINS, "id=abcd") + ) + ), + Arguments.of( + "id=4faf591a-3986-465d-a6ec-538808a0129e", + List.of(new PublicationSearchCriterion(ID, EQUALS, UUID.fromString("4faf591a-3986-465d-a6ec-538808a0129e"))) + ), + Arguments.of( + "category_id=4faf591a-3986-465d-a6ec-538808a0129e", + List.of(new PublicationSearchCriterion(CATEGORY_ID, EQUALS, UUID.fromString("4faf591a-3986-465d-a6ec-538808a0129e"))) + ), + Arguments.of( + "author_id=4faf591a-3986-465d-a6ec-538808a0129e", + List.of(new PublicationSearchCriterion(AUTHOR_ID, EQUALS, UUID.fromString("4faf591a-3986-465d-a6ec-538808a0129e"))) + ) + ); + } + } + + @Nested + public class SplitAndSanitizeSearchCriterion { + @Test + void should_split_criteria_and_remove_duplicates() { + // given + String searchQuery = "criterion1 criterion2 criterion1"; + + // when + Set result = factory.splitAndSanitizeSearchCriterion(searchQuery); + + // then + assertThat(result).containsExactlyInAnyOrder("criterion1", "criterion2"); + } + + @ParameterizedTest + @MethodSource("arguments_of_should_remove_accents_and_split_criteria") + void should_remove_accents_and_split_criteria(String searchQuery, Set expectedResult) { + // when + Set result = factory.splitAndSanitizeSearchCriterion(searchQuery); + + // then + assertThat(result).containsExactlyInAnyOrderElementsOf(expectedResult); + } + + private static Stream arguments_of_should_remove_accents_and_split_criteria() { + return Stream.of( + Arguments.of("critère", Set.of("crit", "re")), + Arguments.of("recherchés", Set.of("recherch")), + Arguments.of("abcdéfghîjklmnöp", Set.of("abcd", "fgh", "jklmn")), + Arguments.of("ædf", Set.of("df")) + ); + } + } +} + diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/ComparisonType.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/ComparisonType.java new file mode 100644 index 0000000..1001d69 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/ComparisonType.java @@ -0,0 +1,8 @@ +package org.codiki.domain.publication.model.search; + +public enum ComparisonType { + EQUALS, + CONTAINS, + BEFORE, + AFTER +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchCriterion.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchCriterion.java new file mode 100644 index 0000000..f0637fb --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchCriterion.java @@ -0,0 +1,7 @@ +package org.codiki.domain.publication.model.search; + +public record PublicationSearchCriterion( + PublicationSearchField searchField, + ComparisonType searchType, + Object value +) { } diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchField.java b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchField.java new file mode 100644 index 0000000..ba4850b --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/model/search/PublicationSearchField.java @@ -0,0 +1,26 @@ +package org.codiki.domain.publication.model.search; + +import java.util.Optional; +import java.util.stream.Stream; + +public enum PublicationSearchField { + ID, + KEY, + TITLE, + TEXT, + DESCRIPTION, + CREATION_DATE, + CATEGORY_ID, + AUTHOR_ID, + AUTHOR_PSEUDO; + + public static Optional from(String fieldName) { + return Optional.ofNullable(fieldName) + .map(String::toUpperCase) + .flatMap(uppercaseFieldName -> + Stream.of(PublicationSearchField.values()) + .filter(field -> field.name().equals(uppercaseFieldName)) + .findFirst() + ); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java b/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java index 090f97d..bfca8bf 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java +++ b/codiki-domain/src/main/java/org/codiki/domain/publication/port/PublicationPort.java @@ -1,9 +1,11 @@ package org.codiki.domain.publication.port; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.codiki.domain.publication.model.Publication; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; public interface PublicationPort { void save(Publication publication); @@ -11,4 +13,6 @@ public interface PublicationPort { Optional findById(UUID publicationId); void delete(Publication publication); + + List search(List criteria); } diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java index d475c37..750a166 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/security/SecurityConfiguration.java @@ -47,6 +47,7 @@ public class SecurityConfiguration { "/api/categories", "/api/pictures/{pictureId}", "/api/publications/{publicationId}", + "/api/publications", "/error" ).permitAll() .requestMatchers( diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/publication/PublicationController.java b/codiki-exposition/src/main/java/org/codiki/exposition/publication/PublicationController.java index a952727..b874143 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/publication/PublicationController.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/publication/PublicationController.java @@ -1,5 +1,6 @@ package org.codiki.exposition.publication; +import java.util.List; import java.util.UUID; import static org.springframework.http.HttpStatus.CREATED; @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -59,4 +61,12 @@ public class PublicationController { public void deletePublication(@PathVariable("publicationId") UUID publicationId) { publicationUseCases.deletePublication(publicationId); } + + @GetMapping + public List searchPublications(@RequestParam("query") String searchQuery) { + return publicationUseCases.searchPublications(searchQuery) + .stream() + .map(PublicationDto::new) + .toList(); + } } diff --git a/codiki-infrastructure/pom.xml b/codiki-infrastructure/pom.xml index 9e183fc..23883b2 100644 --- a/codiki-infrastructure/pom.xml +++ b/codiki-infrastructure/pom.xml @@ -37,5 +37,24 @@ org.postgresql postgresql + + org.apache.commons + commons-lang3 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationJpaAdapter.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationJpaAdapter.java index a0a2548..5483d93 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationJpaAdapter.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationJpaAdapter.java @@ -1,9 +1,11 @@ package org.codiki.infrastructure.publication; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.codiki.domain.publication.model.Publication; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; import org.codiki.domain.publication.port.PublicationPort; import org.codiki.infrastructure.publication.model.PublicationEntity; import org.codiki.infrastructure.publication.repository.PublicationRepository; @@ -12,9 +14,14 @@ import org.springframework.stereotype.Component; @Component public class PublicationJpaAdapter implements PublicationPort { private final PublicationRepository repository; + private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter; - public PublicationJpaAdapter(PublicationRepository repository) { + public PublicationJpaAdapter( + PublicationRepository repository, + PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter + ) { this.repository = repository; + this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter; } @Override @@ -33,4 +40,13 @@ public class PublicationJpaAdapter implements PublicationPort { public void delete(Publication publication) { repository.deleteById(publication.id()); } + + @Override + public List search(List criteria) { + List adaptedCriteria = publicationSearchCriteriaJpaAdapter.adaptCriteriaForJpa(criteria); + return repository.search(adaptedCriteria) + .stream() + .map(PublicationEntity::toDomain) + .toList(); + } } diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapter.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapter.java new file mode 100644 index 0000000..dec2777 --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapter.java @@ -0,0 +1,35 @@ +package org.codiki.infrastructure.publication; + +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.springframework.stereotype.Component; + +@Component +public class PublicationSearchCriteriaJpaAdapter { + public List adaptCriteriaForJpa(List initialCriteria) { + List result = new LinkedList<>(); + + for (PublicationSearchCriterion criterion : initialCriteria) { + boolean criterionAdaptationOccurred = false; + + if (criterion.value() instanceof String criterionValue) { + String unaccentedCriterionValue = StringUtils.stripAccents(criterionValue); + result.add(new PublicationSearchCriterion( + criterion.searchField(), + criterion.searchType(), + unaccentedCriterionValue + )); + criterionAdaptationOccurred = true; + } + + if (!criterionAdaptationOccurred) { + result.add(criterion); + } + } + + return result; + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/model/AuthorEntity.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/model/AuthorEntity.java index 85ca6fa..41d3cff 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/model/AuthorEntity.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/model/AuthorEntity.java @@ -23,7 +23,7 @@ public class AuthorEntity { @Id private UUID id; @Column(nullable = false) - private String name; + private String pseudo; // private String illustrationId; public AuthorEntity(Author author) { @@ -35,6 +35,6 @@ public class AuthorEntity { } public Author toDomain() { - return new Author(id, name, "image"); + return new Author(id, pseudo, "image"); } } diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepository.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepository.java new file mode 100644 index 0000000..a26790c --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepository.java @@ -0,0 +1,10 @@ +package org.codiki.infrastructure.publication.repository; + +import java.util.List; + +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.codiki.infrastructure.publication.model.PublicationEntity; + +public interface CustomPublicationRepository { + List search(List criteria); +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepositoryImpl.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepositoryImpl.java new file mode 100644 index 0000000..3afd2b9 --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/CustomPublicationRepositoryImpl.java @@ -0,0 +1,41 @@ +package org.codiki.infrastructure.publication.repository; + +import java.util.List; + +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.codiki.infrastructure.publication.model.PublicationEntity; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +@Repository +public class CustomPublicationRepositoryImpl implements CustomPublicationRepository { + private final EntityManager entityManager; + private final PublicationPredicateMapper publicationPredicateMapper; + + public CustomPublicationRepositoryImpl(EntityManager entityManager, PublicationPredicateMapper publicationPredicateMapper) { + this.entityManager = entityManager; + this.publicationPredicateMapper = publicationPredicateMapper; + } + + @Override + public List search(final List criteria) { + CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = criteriaBuilder.createQuery(PublicationEntity.class); + Root fromPublication = query.from(PublicationEntity.class); + + Predicate predicate = publicationPredicateMapper.map(criteria, criteriaBuilder, fromPublication); + + CriteriaQuery criteriaQuery = query.select(fromPublication) + .distinct(true) + .where(predicate); + + return entityManager.createQuery(criteriaQuery) + .getResultList(); + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationPredicateMapper.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationPredicateMapper.java new file mode 100644 index 0000000..788ae5d --- /dev/null +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationPredicateMapper.java @@ -0,0 +1,79 @@ +package org.codiki.infrastructure.publication.repository; + +import java.util.List; + +import static org.codiki.domain.publication.model.search.PublicationSearchField.AUTHOR_PSEUDO; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.codiki.domain.publication.model.search.PublicationSearchField; +import org.codiki.infrastructure.publication.model.PublicationEntity; +import org.springframework.stereotype.Component; + +import static jakarta.persistence.criteria.JoinType.LEFT; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +@Component +public class PublicationPredicateMapper { + public Predicate map( + List criteria, + CriteriaBuilder criteriaBuilder, + Root fromPublication + ) { + List criteriaPredicates = criteria.stream() + .map(criterion -> map(criterion, criteriaBuilder, fromPublication)) + .toList(); + return criteriaBuilder.or(criteriaPredicates.toArray(new Predicate[]{})); + } + + private Predicate map( + PublicationSearchCriterion criterion, + CriteriaBuilder criteriaBuilder, + Root fromPublication + ) { + return switch (criterion.searchType()) { + case EQUALS -> mapEqualsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value()); + case CONTAINS -> mapContainsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value()); + default -> null; + }; + } + + private Predicate mapEqualsPredicate( + CriteriaBuilder criteriaBuilder, + Root fromPublication, + PublicationSearchField searchField, + Object value + ) { + From from = fromPublication; + String attributeName = searchField.name().toLowerCase(); + if (searchField == AUTHOR_PSEUDO) { + from = fromPublication.join("author", LEFT); + attributeName = "pseudo"; + } + + return criteriaBuilder.equal( + from.get(attributeName), + value + ); + } + + private Predicate mapContainsPredicate( + CriteriaBuilder criteriaBuilder, + Root fromPublication, + PublicationSearchField searchField, + Object value + ) { + From from = fromPublication; + String attributeName = searchField.name().toLowerCase(); + if (searchField == AUTHOR_PSEUDO) { + from = fromPublication.join("author", LEFT); + attributeName = "pseudo"; + } + + return criteriaBuilder.like( + from.get(attributeName), + String.format("%%%s%%", value) + ); + } +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationRepository.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationRepository.java index fcb16bf..fdcd4ff 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationRepository.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/publication/repository/PublicationRepository.java @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface PublicationRepository extends JpaRepository { +public interface PublicationRepository extends JpaRepository, CustomPublicationRepository { @Query(""" SELECT p FROM PublicationEntity p diff --git a/codiki-infrastructure/src/test/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapterTest.java b/codiki-infrastructure/src/test/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapterTest.java new file mode 100644 index 0000000..f0b448c --- /dev/null +++ b/codiki-infrastructure/src/test/java/org/codiki/infrastructure/publication/PublicationSearchCriteriaJpaAdapterTest.java @@ -0,0 +1,40 @@ +package org.codiki.infrastructure.publication; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS; +import static org.codiki.domain.publication.model.search.PublicationSearchField.KEY; +import org.codiki.domain.publication.model.search.PublicationSearchCriterion; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PublicationSearchCriteriaJpaAdapterTest { + private PublicationSearchCriteriaJpaAdapter adapter; + + @BeforeEach + void setUp() { + adapter = new PublicationSearchCriteriaJpaAdapter(); + } + + @Nested + public class AdaptCriteriaForJpa { + @Test + void should_adapt_criteria_for_jpa() { + // given + List initialCriteria = List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "critère") + ); + + // when + List result = adapter.adaptCriteriaForJpa(initialCriteria); + + // then + List expectedResult = List.of( + new PublicationSearchCriterion(KEY, CONTAINS, "critere") + ); + assertThat(result).isEqualTo(expectedResult); + } + } +} diff --git a/codiki-launcher/src/main/resources/application-local.yml b/codiki-launcher/src/main/resources/application-local.yml index 6d2da3c..7e90d04 100644 --- a/codiki-launcher/src/main/resources/application-local.yml +++ b/codiki-launcher/src/main/resources/application-local.yml @@ -4,6 +4,8 @@ application: temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/ spring: + jpa: + show-sql: true servlet: multipart: max-file-size: 1MB diff --git a/rest-client-collection/Codiki/environments/localhost.bru b/rest-client-collection/Codiki/environments/localhost.bru index 04272c9..9ffb718 100644 --- a/rest-client-collection/Codiki/environments/localhost.bru +++ b/rest-client-collection/Codiki/environments/localhost.bru @@ -1,7 +1,7 @@ vars { url: http://localhost:8080 publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1 - bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA0MjI2NTl9.yHT26uwON5Kk5CWgNvzq2a9OrdJACG4Rk034GPKoZlaxXwK0k8meSHVlrX4ZqTyR3zoL3fm_ujootZRISqOPZw + bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA0OTc2MTd9.xYfS-9CrJxCKUyvZ1ejMKErEttA1zysXxjlVzHFzXJ3ct9dt13xuiI7PHA-8_xY7HQjuIP1M5P2p0OGVsnxXsw categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9 pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972 }