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
}