Skip to content
Merged

Tag #24

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package cat.udl.eps.softarch.demo.controller;

import cat.udl.eps.softarch.demo.domain.Content;
import cat.udl.eps.softarch.demo.domain.Tag;
import cat.udl.eps.softarch.demo.repository.ContentRepository;
import cat.udl.eps.softarch.demo.repository.TagRepository;
import jakarta.transaction.Transactional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@RestController
public class ContentTagController {

private final ContentRepository contentRepository;
private final TagRepository tagRepository;

public ContentTagController(ContentRepository contentRepository, TagRepository tagRepository) {
this.contentRepository = contentRepository;
this.tagRepository = tagRepository;
}

@PutMapping(value = "/contents/{contentId}/tags", consumes = "text/uri-list")
@PreAuthorize("isAuthenticated()")
@Transactional
public ResponseEntity<Void> assignTags(@PathVariable Long contentId, @RequestBody String uriList) {
List<Long> tagIds = parseTagIds(uriList);

Content content = contentRepository.findByContentIdWithTagsForUpdate(contentId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

if (content.getTags() == null) {
content.setTags(new ArrayList<>());
}

for (Long tagId : tagIds) {
Tag tag = tagRepository.findById(tagId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!content.getTags().contains(tag)) {
content.getTags().add(tag);
}
}

contentRepository.save(content);
return ResponseEntity.noContent().build();
}

@DeleteMapping("/contents/{contentId}/tags/{tagId}")
@PreAuthorize("isAuthenticated()")
@Transactional
public ResponseEntity<Void> removeTag(@PathVariable Long contentId, @PathVariable Long tagId) {
Content content = contentRepository.findByContentIdWithTagsForUpdate(contentId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

tagRepository.findById(tagId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

if (content.getTags() != null) {
content.getTags().removeIf(tag -> tagId.equals(tag.getId()));
contentRepository.save(content);
}

return ResponseEntity.noContent().build();
}

@DeleteMapping("/tags/{tagId}/delete")
@PreAuthorize("isAuthenticated()")
@Transactional
public ResponseEntity<Void> deleteTag(@PathVariable Long tagId) {
Tag tag = tagRepository.findById(tagId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

List<Content> taggedContents = contentRepository.findByTags_Id(tagId);
for (Content content : taggedContents) {
if (content.getTags() != null) {
content.getTags().removeIf(existingTag -> tagId.equals(existingTag.getId()));
}
}
contentRepository.saveAll(taggedContents);
tagRepository.delete(tag);

return ResponseEntity.noContent().build();
}

@GetMapping({"/tags/{tagId}/available-contents", "/tags/{tagId}/avaiable-contents"})
public ResponseEntity<List<Content>> getAvailableContents(@PathVariable Long tagId) {
tagRepository.findById(tagId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

return ResponseEntity.ok(contentRepository.findAvailableForTagId(tagId));
}

private List<Long> parseTagIds(String uriList) {
if (uriList == null || uriList.trim().isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}

List<Long> tagIds = Arrays.stream(uriList.split("\\R"))
.map(String::trim)
.filter(line -> !line.isEmpty())
.filter(line -> !line.startsWith("#"))
.map(this::parseTagId)
.toList();

if (tagIds.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return tagIds;
}

private Long parseTagId(String tagUri) {
try {
URI uri = URI.create(tagUri);
String path = uri.getPath();
if (path == null || path.isBlank()) {
throw new IllegalArgumentException("Tag URI has no path");
}

String[] segments = Arrays.stream(path.split("/"))
.filter(segment -> !segment.isBlank())
.toArray(String[]::new);
if (segments.length < 2 || !"tags".equals(segments[segments.length - 2])) {
throw new IllegalArgumentException("Tag URI must point to /tags/{id}");
}

String id = segments[segments.length - 1];
return Long.valueOf(id);
} catch (IllegalArgumentException ex) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Malformed tag URI", ex);
}
}
}
29 changes: 12 additions & 17 deletions src/main/java/cat/udl/eps/softarch/demo/domain/Content.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@

import com.fasterxml.jackson.annotation.JsonIdentityReference;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
Expand All @@ -37,10 +28,17 @@ public class Content {
@Column(nullable = false)
private Project project;*/

/*@ManyToOne
@JsonIdentityReference(alwaysAsId = true)
@Column(nullable = false)
private User user;*/
@ManyToMany
@JoinTable(
name = "content_tags",
joinColumns = @JoinColumn(name = "content_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@NotBlank(message = "Name cannot be empty")
@Column(unique = true, nullable = false)
Expand All @@ -53,13 +51,10 @@ public class Content {
@Column(length = 100)
private String description;

@Column(nullable = false, updatable = false)
private ZonedDateTime createdAt;

@Column(nullable = false, updatable = false)
private ZonedDateTime modifiedAt;

@Enumerated(EnumType.STRING)
@Column(nullable = false) private Visibility visibility;

}
4 changes: 1 addition & 3 deletions src/main/java/cat/udl/eps/softarch/demo/domain/Tag.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cat.udl.eps.softarch.demo.domain;

import com.fasterxml.jackson.annotation.JsonIdentityReference;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
Expand All @@ -24,7 +25,4 @@ public class Tag extends UriEntity<Long> {
private ZonedDateTime created;

private ZonedDateTime modified;

//@ManyToMany(mappedBy = "tags")
//private Set<Content> contentSet = new HashSet<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

import cat.udl.eps.softarch.demo.domain.Content;
//import cat.udl.eps.softarch.demo.domain.Visibility;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.Optional;

Expand All @@ -19,8 +24,23 @@ public interface ContentRepository extends CrudRepository<cat.udl.eps.softarch.d
boolean existsByName(String name);

List<Content> findAll();
List<Content> findByTags_Id(Long tagId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@EntityGraph(attributePaths = "tags")
@Query("select c from Content c where c.contentId = :contentId")
Optional<Content> findByContentIdWithTagsForUpdate(@Param("contentId") Long contentId);

@Query("""
select distinct c from Content c
where c.contentId not in (
select tagged.contentId from Content tagged join tagged.tags t
where t.id = :tagId
)
""")
List<Content> findAvailableForTagId(@Param("tagId") Long tagId);

//List<Content> findByProjectId(Long projectId);

//List<Content> findByProjectIdAndVisibility(Long projectId, Visibility visibility);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface TagRepository extends CrudRepository<Tag, Long> {
boolean existsById(Long id);
boolean existsByName(String name);
Optional<Tag> findByName(String name);
Optional<Tag> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cat.udl.eps.softarch.demo.steps;

import cat.udl.eps.softarch.demo.domain.Tag;
import cat.udl.eps.softarch.demo.domain.Content;
import cat.udl.eps.softarch.demo.domain.User;
import cat.udl.eps.softarch.demo.domain.Visibility;
import cat.udl.eps.softarch.demo.repository.TagRepository;
import cat.udl.eps.softarch.demo.repository.ContentRepository;
import cat.udl.eps.softarch.demo.repository.UserRepository;

import io.cucumber.java.en.*;
import jakarta.transaction.Transactional;

import java.time.ZonedDateTime;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

public class TagOwnershipStepDefs {

private final StepDefs stepDefs;
private final TagRepository tagRepository;
private final ContentRepository contentRepository;
private final UserRepository userRepository;

private Tag tag;
private Content content;

public TagOwnershipStepDefs(StepDefs stepDefs,
TagRepository tagRepository,
ContentRepository contentRepository,
UserRepository userRepository) {
this.stepDefs = stepDefs;
this.tagRepository = tagRepository;
this.contentRepository = contentRepository;
this.userRepository = userRepository;
}

@Given("there is a content {string} created by {string}")
public void thereIsAContentCreatedBy(String contentName, String username) {

User user = userRepository.findById(username)
.orElseThrow(() -> new RuntimeException("User not found"));

content = new Content();
content.setName(contentName);
content.setUser(user);
content.setCreatedAt(ZonedDateTime.now());
content.setModifiedAt(ZonedDateTime.now());
content.setVisibility(Visibility.PUBLIC);

content = contentRepository.save(content);
}

@When("I assign the tag {string} to content {string}")
public void iAssignTagToContent(String tagName, String contentName) throws Exception {

Tag tagEntity = tagRepository.findByName(tagName)
.orElseThrow(() -> new RuntimeException("Tag not found"));

Content contentEntity = contentRepository.findById(content.getContentId())
.orElseThrow(() -> new RuntimeException("Content not found"));

String authenticatedUsername = "creator1";

if (!contentEntity.getUser().getUsername().equals(authenticatedUsername)) {
stepDefs.result = stepDefs.mockMvc.perform(
post("/contents/" + contentEntity.getContentId() + "/tags")
.contentType("text/uri-list")
.content("/tags/" + tagEntity.getId())
.with(user(authenticatedUsername))
).andDo(print())
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isForbidden());
return;
}

stepDefs.result = stepDefs.mockMvc.perform(
post("/contents/" + contentEntity.getContentId() + "/tags")
.contentType("text/uri-list")
.content("/tags/" + tagEntity.getId())
.with(user(authenticatedUsername))
).andDo(print())
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isNoContent());
}

@Then("The response status is {int}")
public void theResponseStatusIs(int status) throws Exception {
stepDefs.result.andExpect(
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().is(status)
);
}

@Then("content {string} should contain tag {string}")
@Transactional
public void contentShouldContainTag(String contentName, String tagName) {

Content updatedContent = contentRepository.findById(content.getContentId())
.orElseThrow(() -> new RuntimeException("Content not found"));

boolean exists = updatedContent.getTags()
.stream()
.anyMatch(t -> t.getName().equals(tagName));

assertTrue(exists, "Tag not assigned correctly to content");
}
}