Skip to content
Snippets Groups Projects
Commit b6cf2052 authored by Nadezhda Zakopailo's avatar Nadezhda Zakopailo
Browse files

Merge branch 'search' into 'dev'

Search

See merge request !1
parents 7837321d 72ee5111
No related branches found
No related tags found
1 merge request!1Search
Showing
with 274 additions and 26 deletions
......@@ -31,6 +31,11 @@
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
</dependencies>
......
......@@ -2,7 +2,9 @@ package cz.cvut.fel.flashcards.CardsMicroservice.controller;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import cz.cvut.fel.flashcards.CardsMicroservice.service.CardBoxService;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardBoxPostDTO;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardPostDTO;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
......@@ -18,7 +20,7 @@ public class CardBoxController {
private final CardBoxService cardBoxService;
@PostMapping
public ResponseEntity<CardBox> createCardBox(@RequestBody CardBox cardBox) {
public ResponseEntity<CardBox> createCardBox(@Valid @RequestBody CardBoxPostDTO cardBox) {
CardBox createdCardBox = cardBoxService.createCardBox(cardBox);
return new ResponseEntity<>(createdCardBox, HttpStatus.CREATED);
}
......@@ -36,7 +38,7 @@ public class CardBoxController {
}
@PutMapping("/{id}")
public ResponseEntity<CardBox> updateCardBox(@PathVariable Long id, @RequestBody CardBox updatedCardBox) {
public ResponseEntity<CardBox> updateCardBox(@PathVariable Long id, @Valid @RequestBody CardBoxPostDTO updatedCardBox) {
CardBox cardBox = cardBoxService.updateCardBox(id, updatedCardBox);
return ResponseEntity.ok(cardBox);
}
......@@ -48,7 +50,7 @@ public class CardBoxController {
}
@PutMapping("/{cardBoxId}/addCard")
public ResponseEntity<CardBox> addCardToCardBox(@PathVariable Long cardBoxId, @RequestBody CardPostDTO postDTO) {
public ResponseEntity<CardBox> addCardToCardBox(@PathVariable Long cardBoxId, @Valid @RequestBody CardPostDTO postDTO) {
CardBox cardBox = cardBoxService.addCardToBox(cardBoxId, postDTO);
return ResponseEntity.ok(cardBox);
}
......@@ -65,4 +67,21 @@ public class CardBoxController {
return ResponseEntity.ok(cardBox);
}
@GetMapping("/search")
public ResponseEntity<List<CardBox>> search(
@RequestParam(required = false) String tag,
@RequestParam(required = false) String title,
@RequestParam(required = false) String keyword
) {
if (tag != null) {
return ResponseEntity.ok(cardBoxService.searchByTag(tag));
} else if (title != null) {
return ResponseEntity.ok(cardBoxService.searchByTitle(title));
} else if (keyword != null) {
return ResponseEntity.ok(cardBoxService.searchByTitleAndTag(keyword));
} else {
return ResponseEntity.badRequest().build();
}
}
}
......@@ -2,6 +2,8 @@ package cz.cvut.fel.flashcards.CardsMicroservice.controller;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.Tag;
import cz.cvut.fel.flashcards.CardsMicroservice.service.TagService;
import cz.cvut.fel.flashcards.CardsMicroservice.util.TagPostDTO;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
......@@ -17,7 +19,7 @@ public class TagController {
private final TagService tagService;
@PostMapping
public ResponseEntity<Tag> createTag(@RequestBody Tag tag) {
public ResponseEntity<Tag> createTag(@Valid @RequestBody TagPostDTO tag) {
Tag createdTag = tagService.createTag(tag);
return new ResponseEntity<>(createdTag, HttpStatus.CREATED);
}
......@@ -35,7 +37,7 @@ public class TagController {
}
@PutMapping("/{id}")
public ResponseEntity<Tag> updateTag(@PathVariable Long id, @RequestBody Tag updatedTag) {
public ResponseEntity<Tag> updateTag(@PathVariable Long id, @Valid @RequestBody TagPostDTO updatedTag) {
Tag tag = tagService.updateTag(id, updatedTag);
return ResponseEntity.ok(tag);
}
......@@ -45,4 +47,10 @@ public class TagController {
tagService.deleteTag(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/search")
public ResponseEntity<List<Tag>> searchByName(@RequestParam String name) {
List<Tag> tags = tagService.searchByName(name);
return ResponseEntity.ok(tags);
}
}
package cz.cvut.fel.flashcards.CardsMicroservice.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
......@@ -18,6 +19,7 @@ public class Card {
private String question;
@ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore
private CardBox cardBox;
}
package cz.cvut.fel.flashcards.CardsMicroservice.entity;
import com.fasterxml.jackson.annotation.*;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import java.util.HashSet;
import java.util.Set;
......@@ -12,6 +12,10 @@ import java.util.Set;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class CardBox {
@Id
......@@ -21,13 +25,18 @@ public class CardBox {
@Column(nullable = false)
private String title;
@Column(nullable = false)
@ColumnDefault(value = "false")
@JsonProperty("isPublic")
private boolean isPublic;
@OneToMany
@OneToMany(fetch = FetchType.LAZY)
@JsonManagedReference
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Set<Card> cards = new HashSet<>();
@ManyToMany
@EqualsAndHashCode.Exclude
private Set<Tag> tags = new HashSet<>();
@Column(nullable = false)
......
package cz.cvut.fel.flashcards.CardsMicroservice.entity;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.*;
import java.util.HashSet;
import java.util.Set;
......@@ -12,6 +14,10 @@ import java.util.Set;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Tag {
@Id
......@@ -24,5 +30,6 @@ public class Tag {
private String color;
@ManyToMany
@EqualsAndHashCode.Exclude
private Set<CardBox> cardBoxes = new HashSet<>();
}
......@@ -2,6 +2,25 @@ package cz.cvut.fel.flashcards.CardsMicroservice.repository;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface CardBoxRepository extends JpaRepository<CardBox, Long> {
@Query("SELECT DISTINCT cb FROM CardBox cb JOIN cb.tags t " +
"WHERE LOWER(t.tagName) LIKE LOWER(CONCAT('%', :tag, '%')) " +
"AND cb.isPublic = true")
List<CardBox> searchCardBoxesByTag(@Param("tag") String tag);
@Query("SELECT DISTINCT cb FROM CardBox cb " +
"WHERE LOWER(cb.title) LIKE LOWER(CONCAT('%', :name, '%')) " +
"AND cb.isPublic = true")
List<CardBox> searchCardBoxesByName(@Param("name") String name);
@Query("SELECT DISTINCT cb FROM CardBox cb LEFT JOIN cb.tags t " +
"WHERE (LOWER(cb.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
"OR LOWER(t.tagName) LIKE LOWER(CONCAT('%', :keyword, '%'))) " +
"AND cb.isPublic = true")
List<CardBox> searchCardBoxesByNameAndTag(@Param("keyword") String keyword);
}
......@@ -2,6 +2,17 @@ package cz.cvut.fel.flashcards.CardsMicroservice.repository;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.Tag;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface TagRepository extends JpaRepository<Tag, Long> {
@Query(
"SELECT DISTINCT t FROM Tag t " +
"WHERE LOWER(t.tagName) LIKE LOWER(CONCAT('%', :name, '%'))"
)
List<Tag> searchByName(@Param("name") String name);
boolean existsByTagName(String tagName);
}
......@@ -4,7 +4,10 @@ import cz.cvut.fel.flashcards.CardsMicroservice.entity.Card;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.Tag;
import cz.cvut.fel.flashcards.CardsMicroservice.repository.CardBoxRepository;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardBoxMapper;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardBoxPostDTO;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardPostDTO;
import jakarta.persistence.EntityNotFoundException;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -20,26 +23,26 @@ public class CardBoxService {
private final CardService cardService;
private final TagService tagService;
public CardBox createCardBox(CardBox cardBox) {
return cardBoxRepository.save(cardBox);
public CardBox createCardBox(CardBoxPostDTO cardBox) {
return cardBoxRepository.save(CardBoxMapper.getInstance().toCardBox(cardBox));
}
public CardBox getCardBoxById(Long id) {
return cardBoxRepository.findById(id)
.orElseThrow(() -> new RuntimeException("CardBox not found with id: " + id));
.orElseThrow(() -> new EntityNotFoundException("CardBox not found with id: " + id));
}
public List<CardBox> getAllCardBoxes() {
return cardBoxRepository.findAll();
}
public CardBox updateCardBox(Long id, CardBox updatedCardBox) {
public CardBox updateCardBox(Long id, CardBoxPostDTO updatedCardBox) {
CardBox existingCardBox = getCardBoxById(id);
existingCardBox.setTitle(updatedCardBox.getTitle());
existingCardBox.setPublic(updatedCardBox.isPublic());
existingCardBox.setOwnerId(updatedCardBox.getOwnerId());
existingCardBox.setCards(updatedCardBox.getCards());
existingCardBox.setTags(updatedCardBox.getTags());
// existingCardBox.setCards(updatedCardBox.getCards());
// existingCardBox.setTags(updatedCardBox.getTags());
return cardBoxRepository.save(existingCardBox);
}
......@@ -69,4 +72,16 @@ public class CardBoxService {
cardBox.getTags().remove(tag);
return cardBoxRepository.save(cardBox);
}
public List<CardBox> searchByTag(String tag) {
return cardBoxRepository.searchCardBoxesByTag(tag);
}
public List<CardBox> searchByTitle(String title) {
return cardBoxRepository.searchCardBoxesByName(title);
}
public List<CardBox> searchByTitleAndTag(String keyword) {
return cardBoxRepository.searchCardBoxesByNameAndTag(keyword);
}
}
......@@ -5,6 +5,7 @@ import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import cz.cvut.fel.flashcards.CardsMicroservice.repository.CardRepository;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardMapper;
import cz.cvut.fel.flashcards.CardsMicroservice.util.CardPostDTO;
import jakarta.persistence.EntityNotFoundException;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -29,7 +30,7 @@ public class CardService {
public Card getCardById(Long id) {
return cardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Card not found with id: " + id));
.orElseThrow(() -> new EntityNotFoundException("Card not found with id: " + id));
}
public List<Card> getAllCards() {
......
......@@ -3,6 +3,10 @@ package cz.cvut.fel.flashcards.CardsMicroservice.service;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.Tag;
import cz.cvut.fel.flashcards.CardsMicroservice.repository.TagRepository;
import cz.cvut.fel.flashcards.CardsMicroservice.util.TagMapper;
import cz.cvut.fel.flashcards.CardsMicroservice.util.TagPostDTO;
import cz.cvut.fel.flashcards.CardsMicroservice.util.exception.DuplicateTagException;
import jakarta.persistence.EntityNotFoundException;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -16,20 +20,26 @@ public class TagService {
private final TagRepository tagRepository;
public Tag createTag(Tag tag) {
return tagRepository.save(tag);
public Tag createTag(TagPostDTO tag) {
if (tagRepository.existsByTagName(tag.getTagName())) {
throw new DuplicateTagException("Tag with this name already exists: " + tag.getTagName());
}
return tagRepository.save(TagMapper.getInstance().toTag(tag));
}
public Tag getTagById(Long id) {
return tagRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tag not found with id: " + id));
.orElseThrow(() -> new EntityNotFoundException("Tag not found with id: " + id));
}
public Tag updateTag(Long id, Tag updatedTag) {
public Tag updateTag(Long id, TagPostDTO updatedTag) {
if (tagRepository.existsByTagName(updatedTag.getTagName())) {
throw new DuplicateTagException("Tag with this name already exists: " + updatedTag.getTagName());
}
Tag existingTag = getTagById(id);
existingTag.setTagName(updatedTag.getTagName());
existingTag.setColor(updatedTag.getColor());
existingTag.setCardBoxes(updatedTag.getCardBoxes());
// existingTag.setCardBoxes(updatedTag.getCardBoxes());
return tagRepository.save(existingTag);
}
......@@ -53,4 +63,8 @@ public class TagService {
public List<Tag> getAllTags() {
return tagRepository.findAll();
}
public List<Tag> searchByName(String name) {
return tagRepository.searchByName(name);
}
}
package cz.cvut.fel.flashcards.CardsMicroservice.util;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.CardBox;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface CardBoxMapper {
CardBoxMapper INSTANCE = Mappers.getMapper(CardBoxMapper.class);
CardBox toCardBox(CardBoxPostDTO cardBoxPostDTO);
CardBoxPostDTO toCardBoxPostDTO(CardBox cardBox);
static CardBoxMapper getInstance() { return INSTANCE; }
}
package cz.cvut.fel.flashcards.CardsMicroservice.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CardBoxPostDTO {
@NotBlank(message = "title cannot be blank")
private String title;
@JsonProperty("isPublic")
private boolean isPublic;
@NotNull(message = "owner id cannot be blank")
private Long ownerId;
}
package cz.cvut.fel.flashcards.CardsMicroservice.util;
import cz.cvut.fel.flashcards.CardsMicroservice.entity.Tag;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface TagMapper {
TagMapper INSTANCE = Mappers.getMapper(TagMapper.class);
Tag toTag(TagPostDTO tagPostDTO);
TagPostDTO toTagPostDTO(Tag tag);
static TagMapper getInstance() { return INSTANCE; }
}
package cz.cvut.fel.flashcards.CardsMicroservice.util;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TagPostDTO {
@NotBlank(message = "title cannot be blank")
private String tagName;
private String color;
}
package cz.cvut.fel.flashcards.CardsMicroservice.util.exception;
public class DuplicateTagException extends RuntimeException {
public DuplicateTagException(String message) {
super(message);
}
}
package cz.cvut.fel.flashcards.CardsMicroservice.util.exception;
import java.time.LocalDateTime;
public record ErrorResponse(
int status,
String message,
LocalDateTime timestamp
) {
}
package cz.cvut.fel.flashcards.CardsMicroservice.util.exception;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFoundException(EntityNotFoundException e) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
e.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
ErrorResponse response = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation error(s): " + errorMessage,
LocalDateTime.now()
);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(DuplicateTagException.class)
public ResponseEntity<ErrorResponse> handleDuplicateEmail(DuplicateTagException ex) {
ErrorResponse response = new ErrorResponse(
HttpStatus.CONFLICT.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
}
}
......@@ -14,4 +14,5 @@ spring:
format_sql: 'true'
hibernate:
ddl-auto: create
# ddl-auto: update
show-sql: 'true'
\ No newline at end of file
......@@ -34,6 +34,12 @@
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
</dependencies>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment