diff --git a/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java b/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java index de837609cbd..637406f0fc3 100644 --- a/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java +++ b/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java @@ -20,11 +20,17 @@ import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; +import io.meeds.social.category.model.CategoryObject; import io.meeds.social.category.plugin.CategoryPlugin; @Component @@ -35,6 +41,9 @@ public class SpaceCategoryPlugin implements CategoryPlugin { @Autowired private SpaceService spaceService; + @Autowired + private IdentityManager identityManager; + @Override public String getType() { return OBJECT_TYPE; @@ -55,4 +64,63 @@ public List getCategoryIds() { return spaceService.getSpaceCategoryIds(); } + /** + * Normalizes the identifier carried by the incoming {@link CategoryObject} to + * the canonical space technical id ({@link Space#getSpaceId()}) before the + * link is stored, read back or broadcast. Category links are stored generically + * on {@code (type, id)}, but the space UI/REST relies on the denormalized + * {@code space.getCategoryIds()} that is only kept in sync by + * {@code CategoryLinkModifiedListener}, whose SPACE branch resolves the space + * through {@code spaceService.getSpaceById(object.getId())} and therefore + * requires the technical id. Callers (e.g. the MCP tooling) may pass the space + * identity id or its pretty name instead; normalizing here makes the write, the + * read-back and the denormalization listener all agree whichever id form was + * supplied. + * + * @param categoryObject The incoming {@link CategoryObject} whose id may be a + * space technical id, a social {@link Identity} id or a pretty name + * @return A {@link CategoryObject} carrying the canonical space technical id, + * or the object unchanged when the id cannot be resolved to a space + **/ + @Override + public CategoryObject getObject(CategoryObject categoryObject) { + Space space = resolveSpace(categoryObject.getId()); + if (space == null) { + return categoryObject; + } + return new CategoryObject(categoryObject.getType(), + String.valueOf(space.getSpaceId()), + categoryObject.getParentId(), + categoryObject.getSpaceId()); + } + + /** + * Resolves a {@link Space} from any of the id forms a space can be referenced + * by across the platform: a space technical id, a social {@link Identity} id + * (mapped to its space through the space identity provider's remote id, i.e. + * the space pretty name) or a pretty name. This mirrors the space resolution + * idiom used by {@code SpacePermanentLinkPlugin#getDirectAccessUrl}. + * + * @param id A space technical id, social {@link Identity} id or pretty name + * @return The resolved {@link Space}, or {@code null} when none matches + **/ + private Space resolveSpace(String id) { + if (StringUtils.isBlank(id)) { + return null; + } + if (StringUtils.isNumeric(id)) { + Space space = spaceService.getSpaceById(id); + if (space != null) { + return space; + } + // Fall back to a social Identity id pointing at the space + Identity identity = identityManager.getIdentity(id); + if (identity != null && SpaceIdentityProvider.NAME.equals(identity.getProviderId())) { + return spaceService.getSpaceByPrettyName(identity.getRemoteId()); + } + return null; + } + return spaceService.getSpaceByPrettyName(id); + } + } diff --git a/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java b/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java index b7690bf8412..70156f39069 100644 --- a/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java +++ b/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.Locale; +import org.apache.commons.collections4.CollectionUtils; import org.junit.Test; import org.exoplatform.commons.ObjectAlreadyExistsException; @@ -129,4 +130,30 @@ public void testUnLink() { assertFalse(categoryLinkService.isLinked(rootCategory.getId(), object)); } + @Test + @SneakyThrows + public void testLinkBySpaceIdentityIdDenormalizesCategoryIds() { + Space space = new Space(); + space.setRegistration(Space.OPEN); + space.setVisibility(Space.PUBLIC); + space = spaceService.createSpace(space, ROOT_USER); + + Category rootCategory = categoryService.getRootCategory(getAdminGroupIdentityId()); + + // Link the space using its social Identity id (how spaces are referenced + // across the platform) instead of its technical id, as the MCP tooling does + String spaceIdentityId = identityManager.getOrCreateSpaceIdentity(space.getPrettyName()).getId(); + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, spaceIdentityId, space.getSpaceId()); + categoryLinkService.link(rootCategory.getId(), object, ROOT_USER); + + // The link is stored/read-back under the normalized technical id + assertTrue(categoryLinkService.isLinked(rootCategory.getId(), object)); + + // And the denormalization listener kept space.categoryIds in sync + restartTransaction(); + Space updatedSpace = spaceService.getSpaceById(space.getId()); + assertTrue(CollectionUtils.isNotEmpty(updatedSpace.getCategoryIds()) + && updatedSpace.getCategoryIds().contains(rootCategory.getId())); + } + } diff --git a/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java b/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java new file mode 100644 index 00000000000..0cd4c936967 --- /dev/null +++ b/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java @@ -0,0 +1,120 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.space.plugin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.model.Space; +import org.exoplatform.social.core.space.spi.SpaceService; + +import io.meeds.social.category.model.CategoryObject; + +@SpringBootTest(classes = { + SpaceCategoryPlugin.class, +}) +@RunWith(SpringRunner.class) +public class SpaceCategoryPluginUnitTest { + + private static final long SPACE_TECHNICAL_ID = 42l; + + private static final long SPACE_IDENTITY_ID = 74456l; + + private static final String SPACE_PRETTY_NAME = "prettyName"; + + @MockBean + private SpaceService spaceService; + + @MockBean + private IdentityManager identityManager; + + @Autowired + private SpaceCategoryPlugin spaceCategoryPlugin; + + @Test + public void testGetObjectNormalizesTechnicalId() { + Space space = newSpace(); + when(spaceService.getSpaceById(String.valueOf(SPACE_TECHNICAL_ID))).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, String.valueOf(SPACE_TECHNICAL_ID), 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + assertEquals(SpaceCategoryPlugin.OBJECT_TYPE, normalized.getType()); + } + + @Test + public void testGetObjectNormalizesIdentityIdToTechnicalId() { + Space space = newSpace(); + // The identity id is not a space technical id + when(spaceService.getSpaceById(String.valueOf(SPACE_IDENTITY_ID))).thenReturn(null); + Identity identity = mock(Identity.class); + when(identity.getProviderId()).thenReturn(SpaceIdentityProvider.NAME); + when(identity.getRemoteId()).thenReturn(SPACE_PRETTY_NAME); + when(identityManager.getIdentity(String.valueOf(SPACE_IDENTITY_ID))).thenReturn(identity); + when(spaceService.getSpaceByPrettyName(SPACE_PRETTY_NAME)).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, String.valueOf(SPACE_IDENTITY_ID), 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + } + + @Test + public void testGetObjectNormalizesPrettyNameToTechnicalId() { + Space space = newSpace(); + when(spaceService.getSpaceByPrettyName(SPACE_PRETTY_NAME)).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, SPACE_PRETTY_NAME, 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + } + + @Test + public void testGetObjectReturnsSameObjectWhenUnresolvable() { + when(spaceService.getSpaceById("unknown")).thenReturn(null); + when(spaceService.getSpaceByPrettyName("unknown")).thenReturn(null); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, "unknown", 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertSame(object, normalized); + } + + private Space newSpace() { + Space space = new Space(); + space.setId(String.valueOf(SPACE_TECHNICAL_ID)); + space.setPrettyName(SPACE_PRETTY_NAME); + return space; + } + +}