Add a component to display a publication list and fix publication search rest service to handle ids.

This commit is contained in:
Florian THIERRY
2024-08-29 13:56:14 +02:00
parent d9b856bd43
commit b5f881e2c5
13 changed files with 151 additions and 77 deletions

View File

@@ -0,0 +1,9 @@
package org.codiki.domain.publication.exception;
import org.codiki.domain.exception.FunctionnalException;
public class BadPublicationSearchCriterionException extends FunctionnalException {
public BadPublicationSearchCriterionException(String message) {
super(message);
}
}

View File

@@ -13,10 +13,7 @@ import org.codiki.domain.exception.RefreshTokenExpiredException;
import org.codiki.domain.exception.UserDoesNotExistException; import org.codiki.domain.exception.UserDoesNotExistException;
import org.codiki.domain.picture.exception.PictureNotFoundException; import org.codiki.domain.picture.exception.PictureNotFoundException;
import org.codiki.domain.picture.exception.PictureUploadException; import org.codiki.domain.picture.exception.PictureUploadException;
import org.codiki.domain.publication.exception.NoPublicationSearchResultException; import org.codiki.domain.publication.exception.*;
import org.codiki.domain.publication.exception.PublicationEditionException;
import org.codiki.domain.publication.exception.PublicationNotFoundException;
import org.codiki.domain.publication.exception.PublicationUpdateForbiddenException;
import org.codiki.domain.user.exception.UserAlreadyExistsException; import org.codiki.domain.user.exception.UserAlreadyExistsException;
import org.codiki.domain.user.exception.UserCreationException; import org.codiki.domain.user.exception.UserCreationException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -28,6 +25,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
@RestControllerAdvice @RestControllerAdvice
public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler { public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ @ExceptionHandler({
BadPublicationSearchCriterionException.class,
CategoryDeletionException.class, CategoryDeletionException.class,
CategoryEditionException.class, CategoryEditionException.class,
CategoryNotFoundException.class, CategoryNotFoundException.class,

View File

@@ -4,6 +4,7 @@ import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion; 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.model.PublicationSearchJpaCriterion;
import org.codiki.infrastructure.publication.model.PublicationSearchResult; import org.codiki.infrastructure.publication.model.PublicationSearchResult;
import org.codiki.infrastructure.publication.repository.PublicationRepository; import org.codiki.infrastructure.publication.repository.PublicationRepository;
import org.springframework.data.domain.Limit; import org.springframework.data.domain.Limit;
@@ -48,7 +49,7 @@ public class PublicationJpaAdapter implements PublicationPort {
@Override @Override
public List<Publication> search(List<PublicationSearchCriterion> criteria) { public List<Publication> search(List<PublicationSearchCriterion> criteria) {
List<PublicationSearchCriterion> adaptedCriteria = publicationSearchCriteriaJpaAdapter.adaptCriteriaForJpa(criteria); List<PublicationSearchJpaCriterion> adaptedCriteria = publicationSearchCriteriaJpaAdapter.adaptCriteriaForJpa(criteria);
return repository.search(adaptedCriteria) return repository.search(adaptedCriteria)
.stream() .stream()
.map(PublicationEntity::toDomain) .map(PublicationEntity::toDomain)

View File

@@ -3,12 +3,15 @@ package org.codiki.infrastructure.publication;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.codiki.domain.publication.exception.BadPublicationSearchCriterionException;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion; import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaCriterion;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaField;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class PublicationSearchCriteriaJpaAdapter { public class PublicationSearchCriteriaJpaAdapter {
public List<PublicationSearchCriterion> adaptCriteriaForJpa(List<PublicationSearchCriterion> initialCriteria) { public List<PublicationSearchJpaCriterion> adaptCriteriaForJpa(List<PublicationSearchCriterion> initialCriteria) {
List<PublicationSearchCriterion> result = new LinkedList<>(); List<PublicationSearchCriterion> result = new LinkedList<>();
for (PublicationSearchCriterion criterion : initialCriteria) { for (PublicationSearchCriterion criterion : initialCriteria) {
@@ -29,6 +32,19 @@ public class PublicationSearchCriteriaJpaAdapter {
} }
} }
return result; return result.stream()
.map(this::mapToJpaCriterion)
.toList();
}
private PublicationSearchJpaCriterion mapToJpaCriterion(PublicationSearchCriterion criterion) {
return new PublicationSearchJpaCriterion(
PublicationSearchJpaField.fromDomain(criterion.searchField())
.orElseThrow(() -> new BadPublicationSearchCriterionException(
String.format("Unknown field research criterion: %s", criterion.searchField()))
),
criterion.searchType(),
criterion.value()
);
} }
} }

View File

@@ -0,0 +1,9 @@
package org.codiki.infrastructure.publication.model;
import org.codiki.domain.publication.model.search.ComparisonType;
public record PublicationSearchJpaCriterion(
PublicationSearchJpaField searchField,
ComparisonType searchType,
Object value
) { }

View File

@@ -0,0 +1,36 @@
package org.codiki.infrastructure.publication.model;
import lombok.Getter;
import org.codiki.domain.publication.model.search.PublicationSearchField;
import java.util.Arrays;
import java.util.Optional;
@Getter
public enum PublicationSearchJpaField {
ID,
KEY,
TITLE,
TEXT,
DESCRIPTION,
CREATION_DATE("creationDate"),
CATEGORY_ID("categoryId"),
AUTHOR_ID("author.id"),
AUTHOR_PSEUDO("author.pseudo");
private final String fieldName;
PublicationSearchJpaField() {
this.fieldName = name().toLowerCase();
}
PublicationSearchJpaField(String fieldName) {
this.fieldName = fieldName;
}
public static Optional<PublicationSearchJpaField> fromDomain(PublicationSearchField publicationSearchField) {
return Arrays.stream(values())
.filter(field -> field.name().equals(publicationSearchField.name()))
.findFirst();
}
}

View File

@@ -1,10 +1,10 @@
package org.codiki.infrastructure.publication.repository; package org.codiki.infrastructure.publication.repository;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaCriterion;
import java.util.List; import java.util.List;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.infrastructure.publication.model.PublicationEntity;
public interface CustomPublicationRepository { public interface CustomPublicationRepository {
List<PublicationEntity> search(List<PublicationSearchCriterion> criteria); List<PublicationEntity> search(List<PublicationSearchJpaCriterion> criteria);
} }

View File

@@ -1,16 +1,15 @@
package org.codiki.infrastructure.publication.repository; 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.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaCriterion;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository @Repository
public class CustomPublicationRepositoryImpl implements CustomPublicationRepository { public class CustomPublicationRepositoryImpl implements CustomPublicationRepository {
@@ -23,7 +22,7 @@ public class CustomPublicationRepositoryImpl implements CustomPublicationReposit
} }
@Override @Override
public List<PublicationEntity> search(final List<PublicationSearchCriterion> criteria) { public List<PublicationEntity> search(final List<PublicationSearchJpaCriterion> criteria) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<PublicationEntity> query = criteriaBuilder.createQuery(PublicationEntity.class); CriteriaQuery<PublicationEntity> query = criteriaBuilder.createQuery(PublicationEntity.class);

View File

@@ -1,14 +1,16 @@
package org.codiki.infrastructure.publication.repository; package org.codiki.infrastructure.publication.repository;
import java.util.List; import java.util.List;
import java.util.UUID;
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.codiki.infrastructure.publication.model.PublicationEntity;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaCriterion;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaField;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static jakarta.persistence.criteria.JoinType.LEFT; import static jakarta.persistence.criteria.JoinType.LEFT;
import static org.codiki.infrastructure.publication.model.PublicationSearchJpaField.AUTHOR_PSEUDO;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
@@ -17,54 +19,63 @@ import jakarta.persistence.criteria.Root;
@Component @Component
public class PublicationPredicateMapper { public class PublicationPredicateMapper {
public Predicate map( public Predicate map(
List<PublicationSearchCriterion> criteria, List<PublicationSearchJpaCriterion> criteria,
CriteriaBuilder criteriaBuilder, CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication Root<PublicationEntity> fromPublication
) { ) {
List<Predicate> criteriaPredicates = criteria.stream() List<Predicate> criteriaPredicates = criteria.stream()
.map(criterion -> map(criterion, criteriaBuilder, fromPublication)) .map(criterion -> map(criterion, criteriaBuilder, fromPublication))
.toList(); .toList();
return criteriaBuilder.or(criteriaPredicates.toArray(new Predicate[]{})); return criteriaBuilder.or(criteriaPredicates.toArray(new Predicate[]{}));
} }
private Predicate map( private Predicate map(
PublicationSearchCriterion criterion, PublicationSearchJpaCriterion criterion,
CriteriaBuilder criteriaBuilder, CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication Root<PublicationEntity> fromPublication
) { ) {
return switch (criterion.searchType()) { return switch (criterion.searchType()) {
case EQUALS -> mapEqualsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value()); case EQUALS ->
case CONTAINS -> mapContainsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value()); mapEqualsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value());
case CONTAINS ->
mapContainsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value());
default -> null; default -> null;
}; };
} }
private Predicate mapEqualsPredicate( private Predicate mapEqualsPredicate(
CriteriaBuilder criteriaBuilder, CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication, Root<PublicationEntity> fromPublication,
PublicationSearchField searchField, PublicationSearchJpaField searchField,
Object value Object value
) { ) {
Predicate result;
From<?, ?> from = fromPublication; From<?, ?> from = fromPublication;
String attributeName = searchField.name().toLowerCase(); String attributeName = searchField.getFieldName();
if (searchField == AUTHOR_PSEUDO) { if (searchField == AUTHOR_PSEUDO) {
from = fromPublication.join("author", LEFT); from = fromPublication.join("author", LEFT);
attributeName = "pseudo"; attributeName = "pseudo";
} }
return criteriaBuilder.equal( if (value instanceof UUID) {
criteriaBuilder.lower( result = criteriaBuilder.equal(from.get(attributeName), value);
from.get(attributeName) } else {
), result = criteriaBuilder.equal(
value criteriaBuilder.lower(
); from.get(attributeName)
),
value
);
}
return result;
} }
private Predicate mapContainsPredicate( private Predicate mapContainsPredicate(
CriteriaBuilder criteriaBuilder, CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication, Root<PublicationEntity> fromPublication,
PublicationSearchField searchField, PublicationSearchJpaField searchField,
Object value Object value
) { ) {
From<?, ?> from = fromPublication; From<?, ?> from = fromPublication;
String attributeName = searchField.name().toLowerCase(); String attributeName = searchField.name().toLowerCase();
@@ -74,10 +85,10 @@ public class PublicationPredicateMapper {
} }
return criteriaBuilder.like( return criteriaBuilder.like(
criteriaBuilder.lower( criteriaBuilder.lower(
from.get(attributeName) from.get(attributeName)
), ),
String.format("%%%s%%", value) String.format("%%%s%%", value)
); );
} }
} }

View File

@@ -1,14 +1,17 @@
package org.codiki.infrastructure.publication; package org.codiki.infrastructure.publication;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.domain.publication.model.search.PublicationSearchField;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaCriterion;
import org.codiki.infrastructure.publication.model.PublicationSearchJpaField;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; 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.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 { class PublicationSearchCriteriaJpaAdapterTest {
private PublicationSearchCriteriaJpaAdapter adapter; private PublicationSearchCriteriaJpaAdapter adapter;
@@ -24,15 +27,15 @@ class PublicationSearchCriteriaJpaAdapterTest {
void should_adapt_criteria_for_jpa() { void should_adapt_criteria_for_jpa() {
// given // given
List<PublicationSearchCriterion> initialCriteria = List.of( List<PublicationSearchCriterion> initialCriteria = List.of(
new PublicationSearchCriterion(KEY, CONTAINS, "critère") new PublicationSearchCriterion(PublicationSearchField.KEY, CONTAINS, "critère")
); );
// when // when
List<PublicationSearchCriterion> result = adapter.adaptCriteriaForJpa(initialCriteria); List<PublicationSearchJpaCriterion> result = adapter.adaptCriteriaForJpa(initialCriteria);
// then // then
List<PublicationSearchCriterion> expectedResult = List.of( List<PublicationSearchJpaCriterion> expectedResult = List.of(
new PublicationSearchCriterion(KEY, CONTAINS, "crit_re") new PublicationSearchJpaCriterion(PublicationSearchJpaField.KEY, CONTAINS, "crit_re")
); );
assertThat(result).isEqualTo(expectedResult); assertThat(result).isEqualTo(expectedResult);
} }

View File

@@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { Publication } from './model/publication'; import { Publication } from './model/publication';
@@ -20,4 +20,10 @@ export class PublicationRestService {
update(publication: Publication): Promise<void> { update(publication: Publication): Promise<void> {
return lastValueFrom(this.httpClient.put<void>(`/api/publications/${publication.id}`, publication)); return lastValueFrom(this.httpClient.put<void>(`/api/publications/${publication.id}`, publication));
} }
search(searchCriteria: string): Promise<Publication[]> {
let params = new HttpParams();
params = params.set('query', searchCriteria);
return lastValueFrom(this.httpClient.get<Publication[]>('/api/publications', { params }));
}
} }

View File

@@ -1,17 +1,2 @@
<h1>Last publications</h1> <h1>Last publications</h1>
<div class="publication-container"> <app-publication-list [publications$]="publications$"></app-publication-list>
<a *ngFor="let publication of publications$ | async" [routerLink]="['/publications/' + publication.id]" class="publication">
<img src="/pictures/{{ publication.illustrationId }}"/>
<div class="body">
<h1>{{publication.title}}</h1>
<h2>{{publication.description}}</h2>
</div>
<div class="footer">
<img src="/pictures/{{ publication.author.image }}" [matTooltip]="publication.author.name"/>
Publication posted by {{publication.author.name}}
<span class="publication-date">
({{ publication.creationDate | date: 'short' : 'fr-Fr' }})
</span>
</div>
</a>
</div>

View File

@@ -5,11 +5,12 @@ import { Publication } from '../../core/rest-services/publications/model/publica
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { PublicationListComponent } from '../../components/publication-list/publication-list.component';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [CommonModule, RouterModule, MatTooltipModule], imports: [PublicationListComponent],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
providers: [HomeService] providers: [HomeService]