From 581acce56b53817fface796ace74de7795ddac58 Mon Sep 17 00:00:00 2001 From: Marc Sladek Date: Sat, 11 Jul 2026 13:22:40 +0200 Subject: [PATCH 1/4] CelTagService.setTags --- .../java/com/celements/tag/CelTagService.java | 79 +++++++--- .../com/celements/tag/CelTagServiceTest.java | 140 ++++++++++++++++++ 2 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java diff --git a/celements-tag/src/main/java/com/celements/tag/CelTagService.java b/celements-tag/src/main/java/com/celements/tag/CelTagService.java index da827d44..2d936f1a 100644 --- a/celements-tag/src/main/java/com/celements/tag/CelTagService.java +++ b/celements-tag/src/main/java/com/celements/tag/CelTagService.java @@ -1,8 +1,9 @@ package com.celements.tag; import static com.celements.common.lambda.LambdaExceptionUtil.*; +import static com.google.common.base.Preconditions.*; +import static com.google.common.base.Predicates.*; import static com.google.common.base.Strings.*; -import static java.util.stream.Collectors.*; import java.util.ArrayList; import java.util.Collection; @@ -12,10 +13,12 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.slf4j.Logger; @@ -24,6 +27,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Service; +import org.xwiki.model.reference.EntityReference; import com.celements.common.lambda.Try; import com.celements.model.field.XObjectFieldAccessor; @@ -32,6 +36,7 @@ import com.celements.tag.classdefs.CelTagClass; import com.celements.tag.providers.CelTagsProvider; import com.celements.tag.providers.CelTagsProvider.CelTagsProvisionException; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.xpn.xwiki.doc.XWikiDocument; @@ -59,14 +64,19 @@ public CelTagService( @NotNull public Optional getTag(@Nullable String type, @Nullable String name) { - return getTagsByType().get(type).stream() + return getTagsByType().get(normaliseType(type)).stream() .filter(tag -> tag.getName().equals(name)) .findFirst(); } @NotNull public StreamEx streamTags(@Nullable String type) { - return StreamEx.of(getTagsByType().get(nullToEmpty(type).toLowerCase())); + return StreamEx.of(getTagsByType().get(normaliseType(type))); + } + + @NotNull + public StreamEx streamTags(@Nullable String type, @Nullable EntityReference scope) { + return streamTags(type).filter(tag -> tag.hasScope(scope)); } @NotNull @@ -98,12 +108,12 @@ protected void runInternal() { } private Multimap collectAllTags() throws CelTagsProvisionException { - List tagBuilders = beanFactory + List tagBuilders = new ArrayList<>(beanFactory .getBeansOfType(CelTagsProvider.class) .values().stream() .map(rethrow(CelTagsProvider::get)) .flatMap(Collection::stream) - .collect(toList()); + .toList()); LOGGER.info("collectAllTags - {}", tagBuilders); return topologicalBuild(tagBuilders); } @@ -152,14 +162,15 @@ public StreamEx getDocTags(@NotNull XWikiDocument doc) { public StreamEx getDocTags(@NotNull XWikiDocument doc, @Nullable String type) { XWikiObjectFetcher fetcher = XWikiObjectFetcher.on(doc) .filter(CelTagClass.CLASS_REF); - type = nullToEmpty(type).trim(); + type = normaliseType(type); if (!type.isEmpty()) { fetcher = fetcher.filter(CelTagClass.FIELD_TYPE, type); } return StreamEx.of(fetcher.stream()).flatMap(obj -> getTags( fieldAccessor.get(obj, CelTagClass.FIELD_TYPE), fieldAccessor.get(obj, CelTagClass.FIELD_TAGS) - .map(Set::copyOf).orElse(Set.of()))); + .map(Set::copyOf).orElse(Set.of()))) + .filter(tag -> tag.hasScope(doc.getWikiRef())); } private Stream getTags(Optional type, Set tags) { @@ -172,20 +183,54 @@ private Stream getTags(Optional type, Set tags) { @NotNull public boolean addTags(@NotNull XWikiDocument doc, @NotNull CelTag... tags) { boolean changed = false; - for (var tagsByType : StreamEx.of(tags).groupingBy(CelTag::getType).entrySet()) { - var editor = XWikiObjectEditor.on(doc) - .filter(CelTagClass.FIELD_TYPE, tagsByType.getKey()); - editor.createFirstIfNotExists(); - changed |= editor.editField(CelTagClass.FIELD_TAGS) - .all(() -> StreamEx.of(editor.fetch().fetchField(CelTagClass.FIELD_TAGS).stream()) - .flatMap(List::stream) - .append(tagsByType.getValue().stream().map(CelTag::getName)) - .distinct() - .toList()); + var groupedTags = StreamEx.of(tags) + .filter(tag -> tag.hasScope(doc.getWikiRef())) + .groupingBy(CelTag::getType); + for (var tagsByType : groupedTags.entrySet()) { + var type = tagsByType.getKey(); + changed |= setTagXObj(doc, type, fetcher -> StreamEx + .of(fetcher.fetchField(CelTagClass.FIELD_TAGS).stream()) + .flatMap(List::stream) + .append(tagsByType.getValue().stream().map(CelTag::getName)) + .distinct() + .toList()); } return changed; } + public boolean setTags(@NotNull XWikiDocument doc, @NotEmpty String type, + @Nullable String... tags) { + var tagType = normaliseType(type); + checkArgument(!tagType.isEmpty(), "tag type cannot be empty"); + var tagNames = normaliseTagNames(tags).toSet(); + return setTagXObj(doc, tagType, fetcher -> streamTags(tagType, doc.getWikiRef()) + .filter(tag -> tagNames.contains(tag.getName())) + .map(CelTag::getName) + .toList()); + } + + private boolean setTagXObj(XWikiDocument doc, String type, + Function> names) { + var editor = XWikiObjectEditor.on(doc) + .filter(CelTagClass.FIELD_TYPE, type); + editor.createFirstIfNotExists(); + return editor.editField(CelTagClass.FIELD_TAGS).all(names.apply(editor.fetch())); + } + + private String normaliseType(@Nullable String type) { + return nullToEmpty(type).trim().toLowerCase(); + } + + private StreamEx normaliseTagNames(@Nullable String... tags) { + return StreamEx.ofNullable(tags) + .flatMap(Stream::of) + .map(Strings::nullToEmpty) + .map(String::trim) + .filter(not(String::isEmpty)) + .map(String::toLowerCase) + .distinct(); + } + @Override public void onApplicationEvent(RefreshEvent event) { LOGGER.trace("onApplicationEvent - {}", event); diff --git a/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java b/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java new file mode 100644 index 00000000..5f93a786 --- /dev/null +++ b/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java @@ -0,0 +1,140 @@ +package com.celements.tag; + +import static com.celements.common.test.CelementsTestUtils.*; +import static com.celements.tag.classdefs.CelTagClass.*; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.WikiReference; + +import com.celements.common.test.AbstractComponentTest; +import com.celements.tag.providers.CelTagsProvider; +import com.celements.wiki.WikiService; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; +import com.xpn.xwiki.objects.classes.StaticListClass; +import com.xpn.xwiki.objects.classes.StringClass; + +import one.util.streamex.StreamEx; + +public class CelTagServiceTest extends AbstractComponentTest { + + private CelTagService service; + private CelTagsProvider providerMock; + + @Before + public void prepare() throws Exception { + providerMock = registerComponentMock(CelTagsProvider.class); + service = getBeanFactory().getBean(CelTagService.class); + var wikiServiceMock = registerComponentMock(WikiService.class); + expect(wikiServiceMock.streamAllWikis()).andReturn(StreamEx.empty()).anyTimes(); + } + + @Test + public void test_addTags() throws Exception { + var tagType = "type"; + var tagNames = List.of("tag1", "tag2", "tag3"); + var tags = StreamEx.of(tagNames) + .map(tagName -> CelTag.builder().type(tagType).name(tagName)) + .toList(); + expect(providerMock.get()).andReturn(tags).anyTimes(); + + var doc = new XWikiDocument(new DocumentReference( + getXContext().getDatabase(), "Space", "TestDoc")); + var tagXObj = new BaseObject(); + tagXObj.setXClassReference(CLASS_REF.getDocRef()); + tagXObj.setStringValue(FIELD_TYPE.getName(), tagType); + tagXObj.setStringValue(FIELD_TAGS.getName(), StreamEx.of(tags) + .limit(2) + .map(t -> t.build().getName()) + .joining("|")); + doc.addXObject(tagXObj); + + var xClass = createBaseClassMock(CLASS_REF.getDocRef()); + expectPropertyClass(xClass, FIELD_TYPE.getName(), new StringClass()); + expectPropertyClass(xClass, FIELD_TAGS.getName(), new StaticListClass()); + + replayDefault(); + assertEquals(2, service.getDocTags(doc).count()); + assertFalse(service.addTags(doc, CelTag.builder().type(tagType).name("tag2").build())); + assertEquals(2, service.getDocTags(doc).count()); + assertTrue(service.addTags(doc, CelTag.builder().type(tagType).name("tag3").build())); + assertEquals(3, service.getDocTags(doc).count()); + assertFalse(service.addTags(doc, CelTag.builder().type(tagType).name("tag2").build())); + assertEquals(3, service.getDocTags(doc).count()); + verifyDefault(); + } + + @Test + public void test_setTags() throws Exception { + var tagType = "type"; + expect(providerMock.get()).andReturn(List.of( + CelTag.builder().type(tagType).name("tag3"))).anyTimes(); + var doc = new XWikiDocument(new DocumentReference( + getXContext().getDatabase(), "Space", "TestDoc")); + var tagXObj = new BaseObject(); + tagXObj.setXClassReference(CLASS_REF.getDocRef()); + tagXObj.setStringValue(FIELD_TYPE.getName(), tagType); + tagXObj.setStringValue(FIELD_TAGS.getName(), "tag1|tag2"); + doc.addXObject(tagXObj); + + var xClass = createBaseClassMock(CLASS_REF.getDocRef()); + expectPropertyClass(xClass, FIELD_TYPE.getName(), new StringClass()); + expectPropertyClass(xClass, FIELD_TAGS.getName(), new StaticListClass()); + + replayDefault(); + assertTrue(service.setTags(doc, tagType, " Tag3 ", "tag3")); + assertEquals("tag3", tagXObj.getStringValue(FIELD_TAGS.getName())); + assertFalse(service.setTags(doc, tagType, "tag3")); + assertTrue(service.setTags(doc, tagType, (String[]) null)); + assertEquals("", tagXObj.getStringValue(FIELD_TAGS.getName())); + assertFalse(service.setTags(doc, tagType, (String[]) null)); + verifyDefault(); + } + + @Test + public void test_scope() throws Exception { + var wiki = new WikiReference(getXContext().getDatabase()); + var otherWiki = new WikiReference("otherwiki"); + var doc = new XWikiDocument(new DocumentReference(wiki.getName(), "Space", "TestDoc")); + var tagType = "type"; + var globalTag = CelTag.builder().type(tagType).name("global"); + var localTag = CelTag.builder().type(tagType).name("local").scope(wiki); + var foreignTag = CelTag.builder().type(tagType).name("foreign").scope(otherWiki); + expect(providerMock.get()).andReturn(List.of(globalTag, localTag, foreignTag)).anyTimes(); + + var tagXObj = new BaseObject(); + tagXObj.setXClassReference(CLASS_REF.getDocRef()); + tagXObj.setStringValue(FIELD_TYPE.getName(), tagType); + tagXObj.setStringValue(FIELD_TAGS.getName(), "global|local|foreign"); + doc.addXObject(tagXObj); + + var xClass = createBaseClassMock(CLASS_REF.getDocRef()); + expectPropertyClass(xClass, FIELD_TYPE.getName(), new StringClass()); + expectPropertyClass(xClass, FIELD_TAGS.getName(), new StaticListClass()); + + replayDefault(); + assertEquals(List.of("global", "local"), service.streamTags(tagType, wiki) + .map(CelTag::getName).toList()); + assertEquals(List.of("global", "foreign"), service.streamTags(tagType, otherWiki) + .map(CelTag::getName).toList()); + assertEquals(List.of("global", "local"), service.getDocTags(doc, tagType) + .map(CelTag::getName).toList()); + assertTrue(service.setTags(doc, tagType, "global", "local", "foreign", "unknown")); + assertEquals("global|local", tagXObj.getStringValue(FIELD_TAGS.getName())); + assertTrue(service.setTags(doc, tagType, "foreign", "unknown")); + assertEquals("", tagXObj.getStringValue(FIELD_TAGS.getName())); + CelTag global = service.getTag(tagType, "global").orElseThrow(); + CelTag local = service.getTag(tagType, "local").orElseThrow(); + CelTag foreign = service.getTag(tagType, "foreign").orElseThrow(); + assertTrue(service.addTags(doc, global, local, foreign)); + assertEquals("global|local", tagXObj.getStringValue(FIELD_TAGS.getName())); + verifyDefault(); + } + +} From 34b90627641c27e00d38bfa19046cac5d7c9c551 Mon Sep 17 00:00:00 2001 From: Marc Sladek Date: Sat, 11 Jul 2026 15:55:14 +0200 Subject: [PATCH 2/4] add getTag with scope --- .../com/celements/tag/CelTagScriptService.java | 3 +-- .../main/java/com/celements/tag/CelTagService.java | 8 ++++++++ .../java/com/celements/tag/CelTagServiceTest.java | 14 +++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/celements-tag/src/main/java/com/celements/tag/CelTagScriptService.java b/celements-tag/src/main/java/com/celements/tag/CelTagScriptService.java index d140853a..b55b841e 100644 --- a/celements-tag/src/main/java/com/celements/tag/CelTagScriptService.java +++ b/celements-tag/src/main/java/com/celements/tag/CelTagScriptService.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import javax.inject.Inject; @@ -48,7 +47,7 @@ public List getTags(String type) { public List getTagNames(String type) { return getTags(type).stream() .map(CelTag::getName) - .collect(Collectors.toList()); + .collect(toList()); } public CelTag getTag(String type, String name) { diff --git a/celements-tag/src/main/java/com/celements/tag/CelTagService.java b/celements-tag/src/main/java/com/celements/tag/CelTagService.java index 2d936f1a..8f12f97c 100644 --- a/celements-tag/src/main/java/com/celements/tag/CelTagService.java +++ b/celements-tag/src/main/java/com/celements/tag/CelTagService.java @@ -69,6 +69,14 @@ public Optional getTag(@Nullable String type, @Nullable String name) { .findFirst(); } + @NotNull + public Optional getTag(@Nullable String type, @Nullable String name, + @Nullable EntityReference scope) { + return streamTags(type, scope) + .filter(tag -> tag.getName().equals(name)) + .findFirst(); + } + @NotNull public StreamEx streamTags(@Nullable String type) { return StreamEx.of(getTagsByType().get(normaliseType(type))); diff --git a/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java b/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java index 5f93a786..c3b12851 100644 --- a/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java +++ b/celements-tag/src/test/java/com/celements/tag/CelTagServiceTest.java @@ -106,7 +106,10 @@ public void test_scope() throws Exception { var globalTag = CelTag.builder().type(tagType).name("global"); var localTag = CelTag.builder().type(tagType).name("local").scope(wiki); var foreignTag = CelTag.builder().type(tagType).name("foreign").scope(otherWiki); - expect(providerMock.get()).andReturn(List.of(globalTag, localTag, foreignTag)).anyTimes(); + var localSharedTag = CelTag.builder().type(tagType).name("shared").scope(wiki); + var foreignSharedTag = CelTag.builder().type(tagType).name("shared").scope(otherWiki); + expect(providerMock.get()).andReturn(List.of( + globalTag, localTag, foreignTag, localSharedTag, foreignSharedTag)).anyTimes(); var tagXObj = new BaseObject(); tagXObj.setXClassReference(CLASS_REF.getDocRef()); @@ -119,10 +122,15 @@ public void test_scope() throws Exception { expectPropertyClass(xClass, FIELD_TAGS.getName(), new StaticListClass()); replayDefault(); - assertEquals(List.of("global", "local"), service.streamTags(tagType, wiki) + assertEquals(List.of("global", "local", "shared"), service.streamTags(tagType, wiki) .map(CelTag::getName).toList()); - assertEquals(List.of("global", "foreign"), service.streamTags(tagType, otherWiki) + assertEquals(List.of("global", "foreign", "shared"), service.streamTags(tagType, otherWiki) .map(CelTag::getName).toList()); + assertEquals(wiki, service.getTag(tagType, "shared", wiki).orElseThrow() + .getScope().orElseThrow()); + assertEquals(otherWiki, service.getTag(tagType, "shared", otherWiki).orElseThrow() + .getScope().orElseThrow()); + assertEquals("global", service.getTag(tagType, "global", wiki).orElseThrow().getName()); assertEquals(List.of("global", "local"), service.getDocTags(doc, tagType) .map(CelTag::getName).toList()); assertTrue(service.setTags(doc, tagType, "global", "local", "foreign", "unknown")); From 6f8333fc4d9d653137dd3b8ac3cb65e68cc38db8 Mon Sep 17 00:00:00 2001 From: Marc Sladek Date: Sun, 12 Jul 2026 23:26:28 +0200 Subject: [PATCH 3/4] CelTagController add scope --- .../main/java/com/celements/tag/CelTag.java | 8 +- .../tag/controller/CelTagController.java | 40 +++++-- .../celements/tag/CelTagControllerTest.java | 103 ++++++++++++++++++ 3 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java diff --git a/celements-tag/src/main/java/com/celements/tag/CelTag.java b/celements-tag/src/main/java/com/celements/tag/CelTag.java index a5091449..ec0417f3 100644 --- a/celements-tag/src/main/java/com/celements/tag/CelTag.java +++ b/celements-tag/src/main/java/com/celements/tag/CelTag.java @@ -2,7 +2,6 @@ import static com.celements.spring.context.SpringContextProvider.*; import static com.google.common.base.Preconditions.*; -import static java.util.stream.Collectors.*; import static one.util.streamex.MoreCollectors.*; import java.util.Comparator; @@ -57,7 +56,7 @@ public static Builder builder() { .getTagsByType() .get(getType()).stream() .filter(tag -> tag.getParent().filter(this::equals).isPresent()) - .collect(toUnmodifiableList())); + .toList()); private CelTag(Builder builder) { checkArgument(builder.hasAllDependencies()); @@ -70,7 +69,7 @@ private CelTag(Builder builder) { this.dependencies = builder.dependencies.values().stream() .filter(Objects::nonNull) .filter(tag -> !tag.getName().equals(builder.parent)) - .collect(toUnmodifiableList()); + .toList(); this.prettyNameForLangGetter = Optional.ofNullable(builder.prettyNameForLangGetter) .orElse(lang -> Optional.empty()); this.depth = (int) getAncestors().count(); @@ -158,8 +157,7 @@ public int hashCode() { public boolean equals(Object obj) { if (this == obj) { return true; - } else if (obj instanceof CelTag) { - CelTag other = (CelTag) obj; + } else if (obj instanceof CelTag other) { return Objects.equals(this.type, other.type) && Objects.equals(this.name, other.name) && Objects.equals(this.scope, other.scope); diff --git a/celements-tag/src/main/java/com/celements/tag/controller/CelTagController.java b/celements-tag/src/main/java/com/celements/tag/controller/CelTagController.java index 8d81b50d..ee09dff5 100644 --- a/celements-tag/src/main/java/com/celements/tag/controller/CelTagController.java +++ b/celements-tag/src/main/java/com/celements/tag/controller/CelTagController.java @@ -1,7 +1,5 @@ package com.celements.tag.controller; -import static java.util.stream.Collectors.*; - import java.util.List; import java.util.Map; import java.util.Optional; @@ -17,13 +15,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.celements.model.context.ModelContext; +import com.celements.model.util.ModelUtils; import com.celements.spring.security.AuthenticatedBaseController; import com.celements.tag.CelTag; import com.celements.tag.CelTagService; import com.celements.web.service.IWebUtilsService; import com.fasterxml.jackson.annotation.JsonInclude; -import one.util.streamex.EntryStream; import one.util.streamex.StreamEx; @RestController @@ -32,66 +31,87 @@ public class CelTagController extends AuthenticatedBaseController { private final CelTagService tagService; + private final ModelContext context; + private final ModelUtils modelUtils; private final IWebUtilsService webUtils; @Inject public CelTagController( CelTagService tagService, + ModelContext context, + ModelUtils modelUtils, IWebUtilsService webUtils) { this.tagService = tagService; + this.context = context; + this.modelUtils = modelUtils; this.webUtils = webUtils; } @GetMapping public Map> getTags() { - return EntryStream.of(tagService.getTagsByType().entries().stream()) - .filterValues(CelTag::isRoot) - .mapValues(TagDto::new) + return tagService.streamAllTags() + .filter(this::isInCurrentScope) + .filter(CelTag::isRoot) + .mapToEntry(CelTag::getType, TagDto::new) .grouping(); } @GetMapping("/types") public Set getTypes() { - return tagService.getTagsByType().keySet(); + return tagService.streamAllTags() + .filter(this::isInCurrentScope) + .map(CelTag::getType) + .toSet(); } @GetMapping("/{type}") public List getTagsByType( @PathVariable String type) { - return tagService.getTagsByType().get(type).stream() + return tagService.streamTags(type) + .filter(this::isInCurrentScope) .filter(CelTag::isRoot) .sorted(CelTag.CMP_ORDER) .map(TagDto::new) - .collect(toList()); + .toList(); } @GetMapping("/{type}/{name}") public ResponseEntity getTagByName( @PathVariable String type, @PathVariable String name) { - return tagService.getTag(type, name) + return tagService.streamTags(type) + .filter(this::isInCurrentScope) + .filter(tag -> tag.getName().equals(name)) + .findFirst() .map(TagDto::new) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } + private boolean isInCurrentScope(CelTag tag) { + return modelUtils.isMainWiki(context.getWikiRef()) || tag.hasScope(context.getWikiRef()); + } + @Immutable @JsonInclude(JsonInclude.Include.NON_EMPTY) public class TagDto { public final String name; + public final String scope; public final int order; public final Map prettyName; public final List children; public TagDto(CelTag tag) { name = tag.getName(); + scope = tag.getScope().map(modelUtils::serializeRef).orElse(null); order = tag.getOrder(); prettyName = StreamEx.of(webUtils.getAllowedLanguages()) .mapToEntry(tag::getPrettyName) .flatMapValues(Optional::stream) .toImmutableMap(); children = tag.getChildren() + .filter(CelTagController.this::isInCurrentScope) .sorted(CelTag.CMP_ORDER) .map(TagDto::new) .toImmutableList(); diff --git a/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java b/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java new file mode 100644 index 00000000..288b7dbf --- /dev/null +++ b/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java @@ -0,0 +1,103 @@ +package com.celements.tag; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.aop.framework.Advised; +import org.xwiki.model.reference.WikiReference; + +import com.celements.auth.user.UserService; +import com.celements.common.test.AbstractComponentTest; +import com.celements.model.context.ModelContext; +import com.celements.model.util.ModelUtils; +import com.celements.tag.controller.CelTagController; +import com.celements.web.service.IWebUtilsService; +import com.google.common.collect.ImmutableMultimap; + +import one.util.streamex.StreamEx; + +public class CelTagControllerTest extends AbstractComponentTest { + + private CelTagController controller; + + @Before + public void prepareTest() throws Exception { + registerComponentMocks( + CelTagService.class, + UserService.class, + ModelContext.class, + ModelUtils.class, + IWebUtilsService.class); + controller = getBeanTarget(CelTagController.class); + } + + @Test + public void test_getTagsByType_subWikiFiltersScope() { + var wiki = new WikiReference("proz"); + var tags = createTags(wiki); + expectContext(wiki, false); + expect(getMock(CelTagService.class).streamTags("type")) + .andReturn(StreamEx.of(tags)); + expectTagDtos(tags); + + replayDefault(); + assertEquals(List.of("global", "local"), controller.getTagsByType("type").stream() + .map(dto -> dto.name) + .toList()); + verifyDefault(); + } + + @Test + public void test_getTagsByType_mainWikiReturnsAllScopes() { + var wiki = new WikiReference("celements2web"); + var tags = createTags(new WikiReference("proz")); + expectContext(wiki, true); + expect(getMock(CelTagService.class).streamTags("type")) + .andReturn(StreamEx.of(tags)); + expectTagDtos(tags); + + replayDefault(); + assertEquals(List.of("global", "local", "foreign"), + controller.getTagsByType("type").stream() + .map(dto -> dto.name) + .toList()); + verifyDefault(); + } + + private List createTags(WikiReference localWiki) { + return List.of( + CelTag.builder().type("type").name("global").build(), + CelTag.builder().type("type").name("local").scope(localWiki).build(), + CelTag.builder().type("type").name("foreign") + .scope(new WikiReference("foreign")).build()); + } + + private void expectContext(WikiReference wiki, boolean mainWiki) { + expect(getMock(ModelContext.class).getWikiRef()).andReturn(wiki).anyTimes(); + expect(getMock(ModelUtils.class).isMainWiki(wiki)).andReturn(mainWiki).anyTimes(); + expect(getMock(IWebUtilsService.class).getAllowedLanguages()).andReturn(List.of()).anyTimes(); + } + + private void expectTagDtos(List tags) { + expect(getMock(CelTagService.class).getTagsByType()) + .andReturn(ImmutableMultimap.builder() + .putAll("type", tags) + .build()) + .anyTimes(); + expect(getMock(ModelUtils.class).serializeRef(anyObject())) + .andAnswer(() -> ((WikiReference) getCurrentArguments()[0]).getName()) + .anyTimes(); + } + + @SuppressWarnings("unchecked") + private T getBeanTarget(Class beanClass) throws Exception { + T bean = getBeanFactory().getBean(beanClass); + return bean instanceof Advised advised + ? (T) advised.getTargetSource().getTarget() + : bean; + } +} From c253ab29da35f1f444a40f39916e8c0127a66f1e Mon Sep 17 00:00:00 2001 From: Marc Sladek Date: Sun, 12 Jul 2026 23:57:01 +0200 Subject: [PATCH 4/4] CelTagControllerTest --- .../celements/tag/CelTagControllerTest.java | 126 +++++++++++++++++- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java b/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java index 288b7dbf..5066ab4b 100644 --- a/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java +++ b/celements-tag/src/test/java/com/celements/tag/CelTagControllerTest.java @@ -4,10 +4,13 @@ import static org.junit.Assert.*; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.junit.Before; import org.junit.Test; import org.springframework.aop.framework.Advised; +import org.springframework.http.HttpStatus; import org.xwiki.model.reference.WikiReference; import com.celements.auth.user.UserService; @@ -45,7 +48,7 @@ public void test_getTagsByType_subWikiFiltersScope() { expectTagDtos(tags); replayDefault(); - assertEquals(List.of("global", "local"), controller.getTagsByType("type").stream() + assertEquals(List.of("local", "global"), controller.getTagsByType("type").stream() .map(dto -> dto.name) .toList()); verifyDefault(); @@ -61,28 +64,139 @@ public void test_getTagsByType_mainWikiReturnsAllScopes() { expectTagDtos(tags); replayDefault(); - assertEquals(List.of("global", "local", "foreign"), + assertEquals(List.of("local", "foreign", "global"), controller.getTagsByType("type").stream() .map(dto -> dto.name) .toList()); verifyDefault(); } + @Test + public void test_getTags_subWikiFiltersScopeAndGroupsRoots() { + var wiki = new WikiReference("proz"); + var tags = createTags(wiki); + var localChild = createChild(tags.get(0), "child", 1, wiki); + var otherType = CelTag.builder().type("other").name("other").scope(wiki).build(); + tags = StreamEx.of(tags).append(localChild, otherType).toList(); + expectContext(wiki, false); + expect(getMock(CelTagService.class).streamAllTags()).andReturn(StreamEx.of(tags)); + expectTagDtos(tags); + + replayDefault(); + var result = controller.getTags(); + assertEquals(List.of("global", "local"), result.get("type").stream() + .map(dto -> dto.name) + .toList()); + assertEquals(List.of("other"), result.get("other").stream() + .map(dto -> dto.name) + .toList()); + assertEquals(List.of("child"), result.get("type").get(0).children.stream() + .map(dto -> dto.name) + .toList()); + verifyDefault(); + } + + @Test + public void test_getTypes_subWikiOnlyReturnsVisibleTypes() { + var wiki = new WikiReference("proz"); + var tags = List.of( + CelTag.builder().type("globalType").name("global").build(), + CelTag.builder().type("localType").name("local").scope(wiki).build(), + CelTag.builder().type("foreignType").name("foreign") + .scope(new WikiReference("foreign")).build()); + expectContext(wiki, false); + expect(getMock(CelTagService.class).streamAllTags()).andReturn(StreamEx.of(tags)); + + replayDefault(); + assertEquals(Set.of("globaltype", "localtype"), controller.getTypes()); + verifyDefault(); + } + + @Test + public void test_getTagByName_subWikiSelectsMatchingScope() { + var wiki = new WikiReference("proz"); + var foreign = CelTag.builder().type("type").name("shared") + .scope(new WikiReference("foreign")).build(); + var local = CelTag.builder().type("type").name("shared").scope(wiki).build(); + var tags = List.of(foreign, local); + expectContext(wiki, false); + expect(getMock(CelTagService.class).streamTags("type")) + .andReturn(StreamEx.of(tags)); + expectTagDtos(tags); + + replayDefault(); + var response = controller.getTagByName("type", "shared"); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("proz", response.getBody().scope); + verifyDefault(); + } + + @Test + public void test_getTagByName_notFound() { + var wiki = new WikiReference("proz"); + var foreign = CelTag.builder().type("type").name("shared") + .scope(new WikiReference("foreign")).build(); + expectContext(wiki, false); + expect(getMock(CelTagService.class).streamTags("type")) + .andReturn(StreamEx.of(foreign)); + + replayDefault(); + assertEquals(HttpStatus.NOT_FOUND, + controller.getTagByName("type", "shared").getStatusCode()); + verifyDefault(); + } + + @Test + public void test_tagDto_subWikiSerializesFieldsAndFiltersChildren() { + var wiki = new WikiReference("proz"); + var root = CelTag.builder().type("type").name("root").order(3) + .prettyName(lang -> "de".equals(lang) ? Optional.of("Wurzel") : Optional.empty()) + .build(); + var localChild = createChild(root, "local", 2, wiki); + var foreignChild = createChild(root, "foreign", 1, new WikiReference("foreign")); + var tags = List.of(root, localChild, foreignChild); + expectContext(wiki, false); + expectTagDtos(tags, List.of("de", "en")); + + replayDefault(); + var dto = controller.new TagDto(root); + assertEquals("root", dto.name); + assertNull(dto.scope); + assertEquals(3, dto.order); + assertEquals("Wurzel", dto.prettyName.get("de")); + assertFalse(dto.prettyName.containsKey("en")); + assertEquals(List.of("local"), dto.children.stream().map(child -> child.name).toList()); + assertEquals("proz", dto.children.get(0).scope); + verifyDefault(); + } + private List createTags(WikiReference localWiki) { return List.of( - CelTag.builder().type("type").name("global").build(), - CelTag.builder().type("type").name("local").scope(localWiki).build(), + CelTag.builder().type("type").name("global").order(3).build(), + CelTag.builder().type("type").name("local").order(1).scope(localWiki).build(), CelTag.builder().type("type").name("foreign") - .scope(new WikiReference("foreign")).build()); + .order(2).scope(new WikiReference("foreign")).build()); + } + + private CelTag createChild(CelTag parent, String name, int order, WikiReference scope) { + var builder = CelTag.builder().type(parent.getType()).name(name).parent(parent.getName()) + .order(order).scope(scope); + builder.addDependency(parent); + return builder.build(); } private void expectContext(WikiReference wiki, boolean mainWiki) { expect(getMock(ModelContext.class).getWikiRef()).andReturn(wiki).anyTimes(); expect(getMock(ModelUtils.class).isMainWiki(wiki)).andReturn(mainWiki).anyTimes(); - expect(getMock(IWebUtilsService.class).getAllowedLanguages()).andReturn(List.of()).anyTimes(); } private void expectTagDtos(List tags) { + expectTagDtos(tags, List.of()); + } + + private void expectTagDtos(List tags, List languages) { + expect(getMock(IWebUtilsService.class).getAllowedLanguages()) + .andReturn(languages).anyTimes(); expect(getMock(CelTagService.class).getTagsByType()) .andReturn(ImmutableMultimap.builder() .putAll("type", tags)