Add search publications use case but it's bugged.

This commit is contained in:
Florian THIERRY
2024-03-15 14:56:34 +01:00
parent 50b305c3cd
commit 6e2b86153e
21 changed files with 589 additions and 7 deletions

View File

@@ -38,6 +38,11 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>

View File

@@ -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<PublicationSearchField> ID_SEARCH_FIELDS = List.of(ID, CATEGORY_ID, AUTHOR_ID);
public List<PublicationSearchCriterion> buildCriteria(String searchQuery) {
Set<String> stringCriteria = Set.of(searchQuery.split(" "));
return stringCriteria.stream()
.map(this::buildPublicationSearchCriterion)
.flatMap(List::stream)
.toList();
}
private List<PublicationSearchCriterion> buildPublicationSearchCriterion(String criterion) {
List<PublicationSearchCriterion> 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<PublicationSearchCriterion> 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<UUID> convertToUuid(String uuidValue) {
Optional<UUID> result;
try {
result = Optional.of(UUID.fromString(uuidValue));
} catch (IllegalArgumentException exception) {
result = Optional.empty();
}
return result;
}
private List<PublicationSearchCriterion> 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<String> splitAndSanitizeSearchCriterion(String searchQuery) {
Set<String> result = new HashSet<>();
for (String fragment : searchQuery.split(" ")) {
Set<String> subFragmentsFromAccentedCharactersSplitting = splitSubFragmentByAccentedCharacters(fragment);
if (isEmpty(subFragmentsFromAccentedCharactersSplitting)) {
result.add(fragment);
} else {
result.addAll(subFragmentsFromAccentedCharactersSplitting);
}
}
return result;
}
private Set<String> splitSubFragmentByAccentedCharacters(String fragment) {
Set<String> result = new HashSet<>();
Matcher accentsMatcher = ACCENT_LETTER_REGEX.matcher(fragment);
Set<String> 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;
}
}

View File

@@ -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<Publication> findById(UUID publicationId) {
return publicationPort.findById(publicationId);
}
public List<Publication> searchPublications(String searchQuery) {
List<PublicationSearchCriterion> criteria = publicationSearchCriteriaFactory.buildCriteria(searchQuery);
return publicationPort.search(criteria);
}
}

View File

@@ -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<PublicationSearchCriterion> expectedResult) {
// when
List<PublicationSearchCriterion> result = factory.buildCriteria(searchQuery);
// then
assertThat(result).isEqualTo(expectedResult);
}
private static Stream<Arguments> 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<String> 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<String> expectedResult) {
// when
Set<String> result = factory.splitAndSanitizeSearchCriterion(searchQuery);
// then
assertThat(result).containsExactlyInAnyOrderElementsOf(expectedResult);
}
private static Stream<Arguments> 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"))
);
}
}
}