Add search publications use case but it's bugged.
This commit is contained in:
@@ -38,6 +38,11 @@
|
|||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>assertj-core</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.codiki.application.publication;
|
|||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -12,7 +13,6 @@ import org.codiki.application.category.CategoryUseCases;
|
|||||||
import org.codiki.application.picture.PictureUseCases;
|
import org.codiki.application.picture.PictureUseCases;
|
||||||
import org.codiki.application.user.UserUseCases;
|
import org.codiki.application.user.UserUseCases;
|
||||||
import org.codiki.domain.category.exception.CategoryNotFoundException;
|
import org.codiki.domain.category.exception.CategoryNotFoundException;
|
||||||
import org.codiki.domain.category.model.Category;
|
|
||||||
import org.codiki.domain.exception.AuthenticationRequiredException;
|
import org.codiki.domain.exception.AuthenticationRequiredException;
|
||||||
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
||||||
import org.codiki.domain.publication.exception.PublicationEditionException;
|
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.Publication;
|
||||||
import org.codiki.domain.publication.model.PublicationEditionRequest;
|
import org.codiki.domain.publication.model.PublicationEditionRequest;
|
||||||
import org.codiki.domain.publication.model.builder.PublicationBuilder;
|
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.publication.port.PublicationPort;
|
||||||
import org.codiki.domain.user.model.User;
|
import org.codiki.domain.user.model.User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -31,8 +32,9 @@ public class PublicationUseCases {
|
|||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
private final PictureUseCases pictureUseCases;
|
private final PictureUseCases pictureUseCases;
|
||||||
private final PublicationPort publicationPort;
|
|
||||||
private final PublicationCreationRequestValidator publicationCreationRequestValidator;
|
private final PublicationCreationRequestValidator publicationCreationRequestValidator;
|
||||||
|
private final PublicationPort publicationPort;
|
||||||
|
private final PublicationSearchCriteriaFactory publicationSearchCriteriaFactory;
|
||||||
private final PublicationUpdateRequestValidator publicationUpdateRequestValidator;
|
private final PublicationUpdateRequestValidator publicationUpdateRequestValidator;
|
||||||
private final UserUseCases userUseCases;
|
private final UserUseCases userUseCases;
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ public class PublicationUseCases {
|
|||||||
PictureUseCases pictureUseCases,
|
PictureUseCases pictureUseCases,
|
||||||
PublicationCreationRequestValidator publicationCreationRequestValidator,
|
PublicationCreationRequestValidator publicationCreationRequestValidator,
|
||||||
PublicationPort publicationPort,
|
PublicationPort publicationPort,
|
||||||
|
PublicationSearchCriteriaFactory publicationSearchCriteriaFactory,
|
||||||
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
|
PublicationUpdateRequestValidator publicationUpdateRequestValidator,
|
||||||
UserUseCases userUseCases
|
UserUseCases userUseCases
|
||||||
) {
|
) {
|
||||||
@@ -54,6 +57,7 @@ public class PublicationUseCases {
|
|||||||
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
|
this.publicationUpdateRequestValidator = publicationUpdateRequestValidator;
|
||||||
this.userUseCases = userUseCases;
|
this.userUseCases = userUseCases;
|
||||||
this.pictureUseCases = pictureUseCases;
|
this.pictureUseCases = pictureUseCases;
|
||||||
|
this.publicationSearchCriteriaFactory = publicationSearchCriteriaFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Publication createPublication(PublicationEditionRequest request) {
|
public Publication createPublication(PublicationEditionRequest request) {
|
||||||
@@ -161,4 +165,10 @@ public class PublicationUseCases {
|
|||||||
public Optional<Publication> findById(UUID publicationId) {
|
public Optional<Publication> findById(UUID publicationId) {
|
||||||
return publicationPort.findById(publicationId);
|
return publicationPort.findById(publicationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Publication> searchPublications(String searchQuery) {
|
||||||
|
List<PublicationSearchCriterion> criteria = publicationSearchCriteriaFactory.buildCriteria(searchQuery);
|
||||||
|
|
||||||
|
return publicationPort.search(criteria);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.codiki.domain.publication.model.search;
|
||||||
|
|
||||||
|
public enum ComparisonType {
|
||||||
|
EQUALS,
|
||||||
|
CONTAINS,
|
||||||
|
BEFORE,
|
||||||
|
AFTER
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.codiki.domain.publication.model.search;
|
||||||
|
|
||||||
|
public record PublicationSearchCriterion(
|
||||||
|
PublicationSearchField searchField,
|
||||||
|
ComparisonType searchType,
|
||||||
|
Object value
|
||||||
|
) { }
|
||||||
@@ -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<PublicationSearchField> from(String fieldName) {
|
||||||
|
return Optional.ofNullable(fieldName)
|
||||||
|
.map(String::toUpperCase)
|
||||||
|
.flatMap(uppercaseFieldName ->
|
||||||
|
Stream.of(PublicationSearchField.values())
|
||||||
|
.filter(field -> field.name().equals(uppercaseFieldName))
|
||||||
|
.findFirst()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.codiki.domain.publication.port;
|
package org.codiki.domain.publication.port;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.domain.publication.model.Publication;
|
import org.codiki.domain.publication.model.Publication;
|
||||||
|
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
|
||||||
|
|
||||||
public interface PublicationPort {
|
public interface PublicationPort {
|
||||||
void save(Publication publication);
|
void save(Publication publication);
|
||||||
@@ -11,4 +13,6 @@ public interface PublicationPort {
|
|||||||
Optional<Publication> findById(UUID publicationId);
|
Optional<Publication> findById(UUID publicationId);
|
||||||
|
|
||||||
void delete(Publication publication);
|
void delete(Publication publication);
|
||||||
|
|
||||||
|
List<Publication> search(List<PublicationSearchCriterion> criteria);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class SecurityConfiguration {
|
|||||||
"/api/categories",
|
"/api/categories",
|
||||||
"/api/pictures/{pictureId}",
|
"/api/pictures/{pictureId}",
|
||||||
"/api/publications/{publicationId}",
|
"/api/publications/{publicationId}",
|
||||||
|
"/api/publications",
|
||||||
"/error"
|
"/error"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.codiki.exposition.publication;
|
package org.codiki.exposition.publication;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.CREATED;
|
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.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -59,4 +61,12 @@ public class PublicationController {
|
|||||||
public void deletePublication(@PathVariable("publicationId") UUID publicationId) {
|
public void deletePublication(@PathVariable("publicationId") UUID publicationId) {
|
||||||
publicationUseCases.deletePublication(publicationId);
|
publicationUseCases.deletePublication(publicationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PublicationDto> searchPublications(@RequestParam("query") String searchQuery) {
|
||||||
|
return publicationUseCases.searchPublications(searchQuery)
|
||||||
|
.stream()
|
||||||
|
.map(PublicationDto::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,5 +37,24 @@
|
|||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<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>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.codiki.infrastructure.publication;
|
package org.codiki.infrastructure.publication;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.codiki.domain.publication.model.Publication;
|
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.domain.publication.port.PublicationPort;
|
||||||
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
import org.codiki.infrastructure.publication.model.PublicationEntity;
|
||||||
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
import org.codiki.infrastructure.publication.repository.PublicationRepository;
|
||||||
@@ -12,9 +14,14 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
public class PublicationJpaAdapter implements PublicationPort {
|
public class PublicationJpaAdapter implements PublicationPort {
|
||||||
private final PublicationRepository repository;
|
private final PublicationRepository repository;
|
||||||
|
private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter;
|
||||||
|
|
||||||
public PublicationJpaAdapter(PublicationRepository repository) {
|
public PublicationJpaAdapter(
|
||||||
|
PublicationRepository repository,
|
||||||
|
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
|
||||||
|
) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
|
this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -33,4 +40,13 @@ public class PublicationJpaAdapter implements PublicationPort {
|
|||||||
public void delete(Publication publication) {
|
public void delete(Publication publication) {
|
||||||
repository.deleteById(publication.id());
|
repository.deleteById(publication.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Publication> search(List<PublicationSearchCriterion> criteria) {
|
||||||
|
List<PublicationSearchCriterion> adaptedCriteria = publicationSearchCriteriaJpaAdapter.adaptCriteriaForJpa(criteria);
|
||||||
|
return repository.search(adaptedCriteria)
|
||||||
|
.stream()
|
||||||
|
.map(PublicationEntity::toDomain)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PublicationSearchCriterion> adaptCriteriaForJpa(List<PublicationSearchCriterion> initialCriteria) {
|
||||||
|
List<PublicationSearchCriterion> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ public class AuthorEntity {
|
|||||||
@Id
|
@Id
|
||||||
private UUID id;
|
private UUID id;
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String name;
|
private String pseudo;
|
||||||
// private String illustrationId;
|
// private String illustrationId;
|
||||||
|
|
||||||
public AuthorEntity(Author author) {
|
public AuthorEntity(Author author) {
|
||||||
@@ -35,6 +35,6 @@ public class AuthorEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Author toDomain() {
|
public Author toDomain() {
|
||||||
return new Author(id, name, "image");
|
return new Author(id, pseudo, "image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PublicationEntity> search(List<PublicationSearchCriterion> criteria);
|
||||||
|
}
|
||||||
@@ -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<PublicationEntity> search(final List<PublicationSearchCriterion> criteria) {
|
||||||
|
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
|
||||||
|
|
||||||
|
CriteriaQuery<PublicationEntity> query = criteriaBuilder.createQuery(PublicationEntity.class);
|
||||||
|
Root<PublicationEntity> fromPublication = query.from(PublicationEntity.class);
|
||||||
|
|
||||||
|
Predicate predicate = publicationPredicateMapper.map(criteria, criteriaBuilder, fromPublication);
|
||||||
|
|
||||||
|
CriteriaQuery<PublicationEntity> criteriaQuery = query.select(fromPublication)
|
||||||
|
.distinct(true)
|
||||||
|
.where(predicate);
|
||||||
|
|
||||||
|
return entityManager.createQuery(criteriaQuery)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<PublicationSearchCriterion> criteria,
|
||||||
|
CriteriaBuilder criteriaBuilder,
|
||||||
|
Root<PublicationEntity> fromPublication
|
||||||
|
) {
|
||||||
|
List<Predicate> 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<PublicationEntity> 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<PublicationEntity> 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<PublicationEntity> 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
public interface PublicationRepository extends JpaRepository<PublicationEntity, UUID> {
|
public interface PublicationRepository extends JpaRepository<PublicationEntity, UUID>, CustomPublicationRepository {
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT p
|
SELECT p
|
||||||
FROM PublicationEntity p
|
FROM PublicationEntity p
|
||||||
|
|||||||
@@ -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<PublicationSearchCriterion> initialCriteria = List.of(
|
||||||
|
new PublicationSearchCriterion(KEY, CONTAINS, "critère")
|
||||||
|
);
|
||||||
|
|
||||||
|
// when
|
||||||
|
List<PublicationSearchCriterion> result = adapter.adaptCriteriaForJpa(initialCriteria);
|
||||||
|
|
||||||
|
// then
|
||||||
|
List<PublicationSearchCriterion> expectedResult = List.of(
|
||||||
|
new PublicationSearchCriterion(KEY, CONTAINS, "critere")
|
||||||
|
);
|
||||||
|
assertThat(result).isEqualTo(expectedResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ application:
|
|||||||
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/
|
temp-path : /Users/florian_thierry/Documents/Developpement/codiki-hexa/pictures-folder/temp/
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
max-file-size: 1MB
|
max-file-size: 1MB
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
vars {
|
vars {
|
||||||
url: http://localhost:8080
|
url: http://localhost:8080
|
||||||
publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1
|
publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1
|
||||||
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA0MjI2NTl9.yHT26uwON5Kk5CWgNvzq2a9OrdJACG4Rk034GPKoZlaxXwK0k8meSHVlrX4ZqTyR3zoL3fm_ujootZRISqOPZw
|
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA0OTc2MTd9.xYfS-9CrJxCKUyvZ1ejMKErEttA1zysXxjlVzHFzXJ3ct9dt13xuiI7PHA-8_xY7HQjuIP1M5P2p0OGVsnxXsw
|
||||||
categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
|
categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
|
||||||
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user