diff --git a/src/main/java/de/holarse/backend/db/repositories/NodeSlugRepository.java b/src/main/java/de/holarse/backend/db/repositories/NodeSlugRepository.java index cbf88e37..f039543c 100644 --- a/src/main/java/de/holarse/backend/db/repositories/NodeSlugRepository.java +++ b/src/main/java/de/holarse/backend/db/repositories/NodeSlugRepository.java @@ -1,6 +1,7 @@ package de.holarse.backend.db.repositories; import de.holarse.backend.db.NodeSlug; +import de.holarse.backend.types.NodeType; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -13,4 +14,6 @@ public interface NodeSlugRepository extends JpaRepository { @Query("FROM NodeSlug ns WHERE ns.nodeId = :nodeId ORDER BY ns.id DESC LIMIT 1") Optional findByNodeId(@Param("nodeId") final int nodeId); + boolean existsByNameAndSlugContext(final String name, final NodeType slugContext); + } diff --git a/src/main/java/de/holarse/web/controller/WorkspaceController.java b/src/main/java/de/holarse/web/controller/WorkspaceController.java index 5065022c..3d83259f 100644 --- a/src/main/java/de/holarse/web/controller/WorkspaceController.java +++ b/src/main/java/de/holarse/web/controller/WorkspaceController.java @@ -12,6 +12,7 @@ import de.holarse.backend.types.NodeType; import de.holarse.web.controller.commands.ArticleForm; import de.holarse.web.defines.WebDefines; +import de.holarse.web.services.SlugService; import jakarta.validation.Valid; import java.security.Principal; import org.slf4j.Logger; @@ -42,6 +43,9 @@ public class WorkspaceController { @Autowired private NodeStatusRepository nodeStatusRepository; + @Autowired + private SlugService slugService; + /** * Übersicht über den eigenen Workspace * @param mv @@ -107,10 +111,7 @@ public ModelAndView saveArticle(@Valid @ModelAttribute("form") final ArticleForm nodeStatus.setPublished(form.isPublished()); // Slug anlegen - final NodeSlug nodeSlug = new NodeSlug(); - nodeSlug.setNodeId(nodeId); - nodeSlug.setName(articleRevision.getTitle1().toLowerCase()); - nodeSlug.setSlugContext(NodeType.article); + final NodeSlug nodeSlug = slugService.slugify(articleRevision); // Artikel anlegen final Article article = new Article(); diff --git a/src/main/java/de/holarse/web/services/SlugService.java b/src/main/java/de/holarse/web/services/SlugService.java index 6ff99ca1..171933b0 100644 --- a/src/main/java/de/holarse/web/services/SlugService.java +++ b/src/main/java/de/holarse/web/services/SlugService.java @@ -1,9 +1,14 @@ package de.holarse.web.services; +import de.holarse.backend.db.Article; +import de.holarse.backend.db.ArticleRevision; +import de.holarse.backend.db.NodeSlug; import de.holarse.backend.db.User; import de.holarse.backend.db.UserSlug; +import de.holarse.backend.db.repositories.NodeSlugRepository; import de.holarse.backend.db.repositories.UserRepository; import de.holarse.backend.db.repositories.UserSlugRepository; +import de.holarse.backend.types.NodeType; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.HashSet; @@ -21,8 +26,40 @@ public class SlugService { private final static transient Logger log = LoggerFactory.getLogger(SlugService.class); + private final static String SLUGWORD_DELIMITER = "_"; + private final static int SLUGWORD_MAXSIZE = 95; + //private final String[] removeWords = new String[]{"a","an","as","at","before","but","by","for","from","is","in","into","like","of","off","on","onto","per","since","than","the","this","that","to","up","via","with"}; + + @Autowired - private UserSlugRepository userSlugRepository; + NodeSlugRepository nodeSlugRepository; + + @Autowired + UserSlugRepository userSlugRepository; + + public NodeSlug slugify(final ArticleRevision articleRevision) { + final String title = articleRevision.getTitle1(); + final String slugifiedTitle = slugify(title); + final List possibleSlugs = new ArrayList<>(101); + possibleSlugs.add(slugifiedTitle); + possibleSlugs.addAll(IntStream.rangeClosed(1, 100).boxed().map(n -> String.format("%s-%d", slugifiedTitle, n)).toList()); + + for (final String possibleSlug : possibleSlugs) { + final boolean result = nodeSlugRepository.existsByNameAndSlugContext(possibleSlug, NodeType.article); + log.debug("User {} testing slug {} exists: {}", title, possibleSlug, result); + if (!result) { + final NodeSlug nodeSlug = new NodeSlug(); + nodeSlug.setName(possibleSlug); + nodeSlug.setNodeId(articleRevision.getNodeId()); + nodeSlug.setSlugContext(NodeType.article); + + return nodeSlug; + } + log.debug("slug {} exists", title, possibleSlug); + } + + throw new IllegalStateException("ran out of article revision slug titles"); + } /** * Hinterlegt ein Slug für diesen Benutzer @@ -49,13 +86,10 @@ public UserSlug slugify(final User user) { } } - throw new IllegalStateException("no slug for user could be found"); + throw new IllegalStateException("ran out of user slug titles"); } - - private final static String SLUGWORD_DELIMITER = "_"; - //private final String[] removeWords = new String[]{"a","an","as","at","before","but","by","for","from","is","in","into","like","of","off","on","onto","per","since","than","the","this","that","to","up","via","with"}; - + public String transliterate(final String title) { return title.toLowerCase() .replaceAll(" of ", " ") @@ -72,8 +106,9 @@ public String transliterate(final String title) { } public String slugify(final String title) { - if (title == null) + if (title == null) { return ""; + } // Ungewollte Wörter raus // Ungewollte Zeichen raus @@ -98,12 +133,13 @@ public String slugify(final String title) { final String[] words = r2.split(" "); // Wörter zusammenfügen, solange keine 95 Zeichen überschritten sind - final StringBuffer buffer = new StringBuffer(95); + final StringBuffer buffer = new StringBuffer(SLUGWORD_MAXSIZE); for(final String word : words) { final String w = word.trim(); //System.out.println("WORD='" + w + "', buffer: '" + buffer.toString() + "', len: " + buffer.length()); - if (StringUtils.isBlank(w)) + if (StringUtils.isBlank(w)) { continue; + } // bisheriger slug + "_" + neues wort if ((buffer.length() + w.length() + 1) > 95) { diff --git a/src/test/java/de/holarse/web/services/SlugServiceTest.java b/src/test/java/de/holarse/web/services/SlugServiceTest.java index 3e523e39..2f909895 100644 --- a/src/test/java/de/holarse/web/services/SlugServiceTest.java +++ b/src/test/java/de/holarse/web/services/SlugServiceTest.java @@ -1,26 +1,88 @@ package de.holarse.web.services; -import static org.junit.jupiter.api.Assertions.assertEquals; +import de.holarse.backend.db.ApiUser; +import de.holarse.backend.db.ArticleRevision; +import de.holarse.backend.db.NodeSlug; +import de.holarse.backend.db.repositories.ApiUserRepository; +import de.holarse.backend.db.repositories.NodeSlugRepository; +import de.holarse.backend.types.NodeType; +import java.util.Arrays; +import java.util.stream.IntStream; +import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.*; +import org.mockito.MockitoAnnotations; public class SlugServiceTest { private SlugService slugService; + @Mock + NodeSlugRepository nodeSlugRepositoryMock; + @BeforeEach public void setup() { + MockitoAnnotations.initMocks(this); slugService = new SlugService(); + slugService.nodeSlugRepository = nodeSlugRepositoryMock; } + @Test + public void testSlugifyArticleHappy() { + final ArticleRevision ar = new ArticleRevision(); + ar.setTitle1("Hallo Welt"); + + when(nodeSlugRepositoryMock.existsByNameAndSlugContext(anyString(), any(NodeType.class))).thenReturn(Boolean.FALSE); // No slug of this name exists + + final NodeSlug expected = new NodeSlug(); + expected.setName("hallo_welt"); + expected.setSlugContext(NodeType.article); + + final NodeSlug result = slugService.slugify(ar); + + assertEquals(expected.getName(), result.getName()); + } + + @Test + public void testSlugifyArticleFirstThreeBlocked() { + final ArticleRevision ar = new ArticleRevision(); + ar.setTitle1("Hallo Welt"); + + when(nodeSlugRepositoryMock.existsByNameAndSlugContext(anyString(), any(NodeType.class))).thenReturn(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); + + + final NodeSlug expected = new NodeSlug(); + expected.setName("hallo_welt-3"); + expected.setSlugContext(NodeType.article); + + final NodeSlug result = slugService.slugify(ar); + + //verify(nodeSlugRepositoryMock, times(3)).existsByNameAndSlugContext("hallo_welt", NodeType.article); + + assertEquals(expected.getName(), result.getName()); + } + + @Test + public void testSlugifyArticleAlllocked() { + final ArticleRevision ar = new ArticleRevision(); + ar.setTitle1("Hallo Welt"); + + when(nodeSlugRepositoryMock.existsByNameAndSlugContext(anyString(), any(NodeType.class))).thenReturn(Boolean.TRUE); + + assertThrows(IllegalStateException.class, () -> slugService.slugify(ar)); + } + @Test public void testSlugifyNull() { String input = null; assertEquals("", slugService.slugify(input), "should not fail"); } -@Test + @Test public void testSlugifyAscii() { assertEquals("abcdefg", slugService.slugify("abcdefg")); }