diff --git a/component/api/src/main/java/io/meeds/social/category/model/Category.java b/component/api/src/main/java/io/meeds/social/category/model/Category.java new file mode 100644 index 00000000000..c81dab35069 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/model/Category.java @@ -0,0 +1,84 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Category implements Cloneable { + + /** + * Technical identifier of the element + */ + private long id; + + /** + * Parent Tree identifier, 0 when it's the root element + */ + private long parentId; + + /** + * The designation of the category, null when no user locale is chosen, else + * it will depends from user Locale + */ + private String name; + + /** + * Fontawesome Icon identifier + */ + private String icon; + + /** + * Identity Id of the category creator + */ + private long creatorId; + + /** + * Identity Id of the owner of the tree (Owner Id who can manage the tree) + */ + private long ownerId; + + /** + * Access Permissions of the category + */ + private List accessPermissionIds; + + /** + * Link/Use Permissions of the category + */ + private List linkPermissionIds; + + @Override + protected Category clone() { // NOSONAR + return new Category(id, + parentId, + name, + icon, + creatorId, + ownerId, + accessPermissionIds, + linkPermissionIds); + } +} diff --git a/component/api/src/main/java/io/meeds/social/category/model/CategoryFilter.java b/component/api/src/main/java/io/meeds/social/category/model/CategoryFilter.java new file mode 100644 index 00000000000..eaecf065a1b --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/model/CategoryFilter.java @@ -0,0 +1,40 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CategoryFilter { + + private long ownerId; + + private long parentId; + + private long depth; + + private long offset; + + private long limit; + +} diff --git a/component/api/src/main/java/io/meeds/social/category/model/CategoryObject.java b/component/api/src/main/java/io/meeds/social/category/model/CategoryObject.java new file mode 100644 index 00000000000..4c680ee769c --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/model/CategoryObject.java @@ -0,0 +1,38 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.model; + +import org.exoplatform.social.metadata.model.MetadataObject; + +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CategoryObject extends MetadataObject { + + public CategoryObject(String type, String id, long spaceId) { + super(type, id, null, spaceId); + } + + public CategoryObject(String type, String id, String parentObjectId, long spaceId) { + super(type, id, parentObjectId, spaceId); + } + +} diff --git a/component/api/src/main/java/io/meeds/social/category/model/CategorySearchFilter.java b/component/api/src/main/java/io/meeds/social/category/model/CategorySearchFilter.java new file mode 100644 index 00000000000..2b970e12b97 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/model/CategorySearchFilter.java @@ -0,0 +1,51 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CategorySearchFilter implements Cloneable { + + private String term; + + private long ownerId; + + private long parentId; + + private long offset; + + private long limit; + + private boolean linkPermission; + + @Override + public CategorySearchFilter clone() { // NOSONAR + return new CategorySearchFilter(term, + ownerId, + parentId, + offset, + limit, + linkPermission); + } +} diff --git a/component/api/src/main/java/io/meeds/social/category/model/CategoryTree.java b/component/api/src/main/java/io/meeds/social/category/model/CategoryTree.java new file mode 100644 index 00000000000..bd6881b3f10 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/model/CategoryTree.java @@ -0,0 +1,45 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.model; + +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CategoryTree extends Category { + + private List categories; + + public CategoryTree(Category category) { + super(category.getId(), + category.getParentId(), + category.getName(), + category.getIcon(), + category.getCreatorId(), + category.getOwnerId(), + category.getAccessPermissionIds(), + category.getLinkPermissionIds()); + } + +} diff --git a/component/api/src/main/java/io/meeds/social/category/plugin/CategoryPlugin.java b/component/api/src/main/java/io/meeds/social/category/plugin/CategoryPlugin.java new file mode 100644 index 00000000000..f705ab28f08 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/plugin/CategoryPlugin.java @@ -0,0 +1,55 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.plugin; + +public interface CategoryPlugin { + + /** + * @return The managed Object Type (Space, Activity...) + **/ + public String getType(); + + /** + * Checks whether an associated object to a category is accessible to a user. + * This check will be called after checking whether the user can access or not + * to the category an object (generic ACL check made in Category API switch + * Category properties) + * + * @id Object technical identifier + * @username User technical name (login identifier) + * @return true if the user has access permission to the designated Object + * Type + **/ + public boolean canAccess(String objectId, String username); + + /** + * Checks whether an associated object to a category is editable to a user. + * This check will be used mainly used to know if a user can modify the object + * in order to associate/link a category into it. This check will be called + * after checking whether the user can link or not to the category an object + * (generic ACL check made in Category API switch Category properties) + * + * @id Object technical identifier + * @username User technical name (login identifier) + * @return true if the user has access permission to the designated Object + * Type identified by its Id + **/ + public boolean canEdit(String objectId, String username); + +} diff --git a/component/api/src/main/java/io/meeds/social/category/service/CategoryLinkService.java b/component/api/src/main/java/io/meeds/social/category/service/CategoryLinkService.java new file mode 100644 index 00000000000..3c0b7933d51 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/service/CategoryLinkService.java @@ -0,0 +1,99 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.service; + +import org.exoplatform.commons.ObjectAlreadyExistsException; +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.model.CategoryObject; + +public interface CategoryLinkService { + + /** + * @param categoryId {@link Category} identifier + * @param object {@link CategoryObject} using type/id to designate any object + * in the platform (Space, Activity ...) + * @return true if the object is linked to the category + */ + boolean isLinked(long categoryId, CategoryObject object); + + /** + * Creates a link from the designated object to the category + * + * @param categoryId {@link Category} identifier + * @param object {@link CategoryObject} using type/id to designate any object + * in the platform (Space, Activity ...) + * @param username User name/login + * @throws ObjectNotFoundException when the category doesn't exist + * @throws ObjectAlreadyExistsException when the object is already linked to + * the category + * @throws IllegalAccessException when the user doesn't have 'Link' permission + * on the category or not having 'edit' permission on the object + */ + void link(long categoryId, CategoryObject object, String username) throws ObjectNotFoundException, + ObjectAlreadyExistsException, + IllegalAccessException; + + /** + * Creates a link from the designated object to the category + * + * @param categoryId {@link Category} identifier + * @param object {@link CategoryObject} using type/id to designate any object + * in the platform (Space, Activity ...) + */ + void link(long categoryId, CategoryObject object); + + /** + * Deletes a link of the designated object from the category + * + * @param categoryId {@link Category} identifier + * @param object {@link CategoryObject} using type/id to designate any object + * in the platform (Space, Activity ...) + * @param username User name/login + * @throws ObjectNotFoundException when the link doesn't exist + * @throws IllegalAccessException when the user doesn't have 'Link' permission + * on the category or not having 'edit' permission on the object + */ + void unlink(long categoryId, CategoryObject object, String username) throws ObjectNotFoundException, IllegalAccessException; + + /** + * Deletes a link of the designated object from the category + * + * @param categoryId {@link Category} identifier + * @param object {@link CategoryObject} using type/id to designate any object + * in the platform (Space, Activity ...) + */ + void unlink(long categoryId, CategoryObject object); + + /** + * @param category {@link Category} + * @param username User name/login + * @return true if can link an object to the category, else false + */ + boolean canManageLink(Category category, String username); + + /** + * @param categoryId {@link Category} identifier + * @param username User name/login + * @return true if can link an object to the category, else false + */ + boolean canManageLink(long categoryId, String username); + +} diff --git a/component/api/src/main/java/io/meeds/social/category/service/CategoryService.java b/component/api/src/main/java/io/meeds/social/category/service/CategoryService.java new file mode 100644 index 00000000000..e772a9c7e34 --- /dev/null +++ b/component/api/src/main/java/io/meeds/social/category/service/CategoryService.java @@ -0,0 +1,144 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.service; + +import java.util.List; +import java.util.Locale; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.social.core.identity.model.Identity; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.model.CategoryFilter; +import io.meeds.social.category.model.CategorySearchFilter; +import io.meeds.social.category.model.CategoryTree; + +public interface CategoryService { + + public static final String EVENT_SOCIAL_CATEGORY_CREATED = "social.category.created"; + + public static final String EVENT_SOCIAL_CATEGORY_UPDATED = "social.category.updated"; + + public static final String EVENT_SOCIAL_CATEGORY_DELETED = "social.category.deleted"; + + public static final String EVENT_SOCIAL_CATEGORY_ITEM_LINKED = "social.category.object.linked"; + + public static final String EVENT_SOCIAL_CATEGORY_ITEM_UNLINKED = "social.category.object.unlinked"; + + /** + * Retrieves a {@link CategoryTree} with its associated categories switch + * designated depth and limit from the filter. This method will return only + * visible categories by the user. + * + * @param filter used filter to query the Category tree + * @param username User name/login + * @param locale used {@link Locale} to retrieve translated name + * @return {@link CategoryTree} switch used filter + */ + CategoryTree getCategoryTree(CategoryFilter filter, String username, Locale locale); + + /** + * Searches Categories in flat mode switch a name. This will return only + * accessible categories by the user. + * + * @param filter used filter to query the Category tree + * @param username User name/login + * @param locale used {@link Locale} to retrieve translated name + * @return a {@link List} of {@link Category} switch used filter + */ + List findCategories(CategorySearchFilter filter, String username, Locale locale); + + /** + * Creates a new {@link Category} + * + * @param category {@link Category} to create + * @param username User name/login + * @return created {@link Category} + * @throws ObjectNotFoundException when the {@link Category} designated by the + * parentId doesn't exist + * @throws IllegalAccessException when the user isn't allowed to edit a + * categories switch the ownerId field + */ + Category createCategory(Category category, String username) throws ObjectNotFoundException, IllegalAccessException; + + /** + * @param category {@link Category} to create + * @param username User name/login + * @return updated {@link Category} + * @throws ObjectNotFoundException when the {@link Category} designated by the + * parentId or the id of the category itself doesn't exist + * @throws IllegalAccessException when the user isn't allowed to edit a + * categories switch the ownerId field + */ + Category updateCategory(Category category, String username) throws ObjectNotFoundException, IllegalAccessException; + + /** + * @param categoryId {@link Category} identifier + * @param username User name/login + * @return deleted {@link Category} + * @throws ObjectNotFoundException when the {@link Category} designated by the + * the id doesn't exist + * @throws IllegalAccessException when the user isn't allowed to edit a + * categories switch the ownerId field + */ + Category deleteCategory(long categoryId, String username) throws ObjectNotFoundException, IllegalAccessException; + + /** + * @param categoryId {@link Category} identifier + * @return {@link Category} if found, else null + */ + Category getCategory(long categoryId); + + /** + * @param ownerId {@link Identity} identifier + * @return the root {@link Category} of the tree if found, else null + */ + Category getRootCategory(long ownerId); + + /** + * @param category {@link Category} + * @param username User name/login + * @return true if can access to the category, else false + */ + boolean canAccess(Category category, String username); + + /** + * @param categoryId {@link Category} identifier + * @param username User name/login + * @return true if can access to the category, else false + */ + boolean canAccess(long categoryId, String username); + + /** + * @param category {@link Category} + * @param username User name/login + * @return true if can edit to the category switch its tree ownerId, else + * false + */ + boolean canEdit(Category category, String username); + + /** + * @param categoryId {@link Category} identifier + * @param username User name/login + * @return true if can edit to the category switch its tree ownerId, else + * false + */ + boolean canEdit(long categoryId, String username); + +} diff --git a/component/api/src/main/java/org/exoplatform/social/core/manager/IdentityManager.java b/component/api/src/main/java/org/exoplatform/social/core/manager/IdentityManager.java index 78ceb732872..188f7cf1440 100644 --- a/component/api/src/main/java/org/exoplatform/social/core/manager/IdentityManager.java +++ b/component/api/src/main/java/org/exoplatform/social/core/manager/IdentityManager.java @@ -18,6 +18,7 @@ import org.exoplatform.commons.file.model.FileItem; import org.exoplatform.commons.utils.ListAccess; +import org.exoplatform.services.organization.Group; import org.exoplatform.social.core.activity.model.ActivityStream; import org.exoplatform.social.core.identity.*; import org.exoplatform.social.core.identity.model.GlobalId; @@ -278,9 +279,15 @@ ListAccess getSpaceIdentityByProfileFilter(Space space, ProfileFilter * @return Null if nothing is found, or the Identity object. * @see #getIdentity(String, boolean) * @LevelAPI Provisional + * @deprecated Use getIdentity(Long) rather than getIdentity(String) since the id is no more an UUID inherited from JCR */ + @Deprecated(forRemoval = true, since = "7.0") Identity getIdentity(String id); + default Identity getIdentity(long id) { + return getIdentity(String.valueOf(id)); + } + /** * Gets an identity by a remote Id. * @@ -582,6 +589,16 @@ default Identity getOrCreateSpaceIdentity(String spacePrettyName) { return getOrCreateIdentity(ActivityStream.SPACE_PROVIDER_ID, spacePrettyName); } + /** + * Retrieves the identity of a given group identified by its full id + * + * @param groupId {@link Group} prettyName + * @return {@link Identity} if found, else null + */ + default Identity getOrCreateGroupIdentity(String groupId) { + return getOrCreateIdentity("group", groupId); + } + /** * Retrieves the identity of a given user identified by his username/login * diff --git a/component/api/src/main/java/org/exoplatform/social/metadata/MetadataService.java b/component/api/src/main/java/org/exoplatform/social/metadata/MetadataService.java index 0baa2a24cc1..23a050da859 100644 --- a/component/api/src/main/java/org/exoplatform/social/metadata/MetadataService.java +++ b/component/api/src/main/java/org/exoplatform/social/metadata/MetadataService.java @@ -95,6 +95,14 @@ public interface MetadataService { */ Metadata getMetadataByKey(MetadataKey metadataKey); + /** + * Retrieves a {@link Metadata} identified by its technical identifier + * + * @param id technical identifier + * @return {@link Metadata} if found, else null + */ + Metadata getMetadataById(long id); + /** * Creates a new Metadata Item. When the metadata with the designated key * doesn't exists, it will create a new one @@ -606,6 +614,15 @@ Map countMetadataItemsByMetadataTypeAndSpacesIdAndCreatorId(String m */ List getMetadatasByProperty(String propertyKey, String propertyValue, long limit); + /** + * @param propertyKey {@link Metadata} property key + * @param propertyValue {@link Metadata} property value + * @param offset offset of results to retrieve + * @param limit limit of results to retrieve + * @return {@link List} of Managed {@link Metadata} by property + */ + List getMetadataIdsByProperty(String propertyKey, String propertyValue, long offset, long limit); + /** * @param metadataTypeName metadata name {@link Metadata} name * @param limit limit of results to retrieve diff --git a/component/api/src/main/java/org/exoplatform/social/metadata/model/MetadataKey.java b/component/api/src/main/java/org/exoplatform/social/metadata/model/MetadataKey.java index 3b5fb3bef3e..37b7464dbde 100644 --- a/component/api/src/main/java/org/exoplatform/social/metadata/model/MetadataKey.java +++ b/component/api/src/main/java/org/exoplatform/social/metadata/model/MetadataKey.java @@ -18,6 +18,8 @@ */ package org.exoplatform.social.metadata.model; +import java.io.Serializable; + import lombok.*; /** @@ -26,13 +28,15 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class MetadataKey implements Cloneable { +public class MetadataKey implements Serializable, Cloneable { + + private static final long serialVersionUID = 7584000823295369142L; - private String type; + private String type; - private String name; + private String name; - private long audienceId; + private long audienceId; @Override public MetadataKey clone() { // NOSONAR diff --git a/component/core/src/main/java/io/meeds/social/category/listener/CategoryModifiedIndexingListener.java b/component/core/src/main/java/io/meeds/social/category/listener/CategoryModifiedIndexingListener.java new file mode 100644 index 00000000000..dc68a97395c --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/listener/CategoryModifiedIndexingListener.java @@ -0,0 +1,76 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.listener; + +import static io.meeds.social.category.service.CategoryService.EVENT_SOCIAL_CATEGORY_CREATED; +import static io.meeds.social.category.service.CategoryService.EVENT_SOCIAL_CATEGORY_DELETED; +import static io.meeds.social.category.service.CategoryService.EVENT_SOCIAL_CATEGORY_UPDATED; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.search.index.IndexingService; +import org.exoplatform.services.listener.Asynchronous; +import org.exoplatform.services.listener.Event; +import org.exoplatform.services.listener.ListenerBase; +import org.exoplatform.services.listener.ListenerService; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.storage.elasticsearch.CategoryIndexingConnector; + +import jakarta.annotation.PostConstruct; + +@Asynchronous +@Component +public class CategoryModifiedIndexingListener implements ListenerBase { + + @Autowired + private ListenerService listenerService; + + @Autowired + private IndexingService indexingService; + + @PostConstruct + public void init() { + listenerService.addListener(EVENT_SOCIAL_CATEGORY_CREATED, this); + listenerService.addListener(EVENT_SOCIAL_CATEGORY_UPDATED, this); + listenerService.addListener(EVENT_SOCIAL_CATEGORY_DELETED, this); + } + + @Override + public void onEvent(Event event) throws Exception { + switch (event.getEventName()) { + case EVENT_SOCIAL_CATEGORY_CREATED: { + indexingService.index(CategoryIndexingConnector.TYPE, String.valueOf(event.getSource().getId())); + break; + } + case EVENT_SOCIAL_CATEGORY_UPDATED: { + indexingService.reindex(CategoryIndexingConnector.TYPE, String.valueOf(event.getSource().getId())); + break; + } + case EVENT_SOCIAL_CATEGORY_DELETED: { + indexingService.unindex(CategoryIndexingConnector.TYPE, String.valueOf(event.getSource().getId())); + break; + } + default: + throw new IllegalArgumentException("Unexpected event name: " + event.getEventName()); + } + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/plugin/CategoryTranslationPlugin.java b/component/core/src/main/java/io/meeds/social/category/plugin/CategoryTranslationPlugin.java new file mode 100644 index 00000000000..8dd059b39a8 --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/plugin/CategoryTranslationPlugin.java @@ -0,0 +1,77 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.plugin; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.service.CategoryService; +import io.meeds.social.translation.plugin.TranslationPlugin; +import io.meeds.social.translation.service.TranslationService; + +import jakarta.annotation.PostConstruct; + +@Component +public class CategoryTranslationPlugin extends TranslationPlugin { + + public static final String OBJECT_TYPE = "category"; + + public static final String NAME_FIELD = "name"; + + @Autowired + private TranslationService translationService; + + @Autowired + private CategoryService categoryService; + + @PostConstruct + public void init() { + translationService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasAccessPermission(long categoryId, String username) throws ObjectNotFoundException { + return categoryService.canAccess(categoryId, username); + } + + @Override + public boolean hasEditPermission(long categoryId, String username) throws ObjectNotFoundException { + return categoryService.canEdit(categoryId, username); + } + + @Override + public long getAudienceId(long categoryId) throws ObjectNotFoundException { + Category category = categoryService.getCategory(categoryId); + return category == null ? 0 : category.getOwnerId(); + } + + @Override + public long getSpaceId(long categoryId) throws ObjectNotFoundException { + return 0; + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/service/CategoryLinkServiceImpl.java b/component/core/src/main/java/io/meeds/social/category/service/CategoryLinkServiceImpl.java new file mode 100644 index 00000000000..185a0c319b6 --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/service/CategoryLinkServiceImpl.java @@ -0,0 +1,137 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.service; + +import static io.meeds.social.category.utils.Utils.isMemberOf; + +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.commons.ObjectAlreadyExistsException; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.spi.SpaceService; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.model.CategoryObject; +import io.meeds.social.category.plugin.CategoryPlugin; +import io.meeds.social.category.storage.CategoryStorage; + +@Service +public class CategoryLinkServiceImpl implements CategoryLinkService { + + @Autowired + private IdentityManager identityManager; + + @Autowired + private CategoryStorage categoryStorage; + + @Autowired + private SpaceService spaceService; + + @Autowired + private UserACL userAcl; + + @Autowired + private List categoryPlugins; + + private long superUserIdentityId; + + @Override + public boolean isLinked(long categoryId, CategoryObject object) { + return categoryStorage.isLinked(categoryId, object); + } + + @Override + public void link(long categoryId, CategoryObject object, String username) throws ObjectNotFoundException, + ObjectAlreadyExistsException, + IllegalAccessException { + checkCanLink(categoryId, object, username); + long userIdentityId = Long.parseLong(identityManager.getOrCreateUserIdentity(username).getId()); + categoryStorage.link(categoryId, object, userIdentityId); + } + + @Override + public void link(long categoryId, CategoryObject object) { + categoryStorage.link(categoryId, object, getSuperUserIdentityId()); + } + + @Override + public void unlink(long categoryId, CategoryObject object, String username) throws ObjectNotFoundException, + IllegalAccessException { + checkCanLink(categoryId, object, username); + categoryStorage.unlink(categoryId, object); + } + + @Override + public void unlink(long categoryId, CategoryObject object) { + categoryStorage.unlink(categoryId, object); + } + + @Override + public boolean canManageLink(long categoryId, String username) { + return canManageLink(categoryStorage.getCategory(categoryId), username); + } + + @Override + public boolean canManageLink(Category category, String username) { + if (category == null || CollectionUtils.isEmpty(category.getLinkPermissionIds())) { + return false; + } else { + org.exoplatform.services.security.Identity userAclIdentity = userAcl.getUserIdentity(username); + return userAcl.isAdministrator(userAclIdentity) + || category.getLinkPermissionIds() + .stream() + .anyMatch(id -> isMemberOf(identityManager, + spaceService, + userAcl, + id, + username)); + } + } + + private void checkCanLink(long categoryId, CategoryObject object, String username) throws ObjectNotFoundException, + IllegalAccessException { + Category category = categoryStorage.getCategory(categoryId); + if (category == null) { + throw new ObjectNotFoundException(String.format("Category with id %s doesn't exist", categoryId)); + } + if (!canManageLink(category, username)) { + throw new IllegalAccessException(String.format("Category with id %s doesn't exist", categoryId)); + } + if (categoryPlugins == null + || categoryPlugins.stream() + .noneMatch(p -> StringUtils.equals(p.getType(), object.getType()) + && p.canEdit(object.getId(), username))) { + throw new IllegalStateException(String.format("Object with type %s isn't managed by categories", object.getType())); + } + } + + private long getSuperUserIdentityId() { + if (superUserIdentityId == 0) { + superUserIdentityId = Long.parseLong(identityManager.getOrCreateUserIdentity(userAcl.getSuperUser()).getId()); + } + return superUserIdentityId; + } +} diff --git a/component/core/src/main/java/io/meeds/social/category/service/CategoryServiceImpl.java b/component/core/src/main/java/io/meeds/social/category/service/CategoryServiceImpl.java new file mode 100644 index 00000000000..dc1a420ddfa --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/service/CategoryServiceImpl.java @@ -0,0 +1,406 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.service; + +import static io.meeds.social.category.utils.Utils.isManagerOf; +import static io.meeds.social.category.utils.Utils.isMemberOf; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.SpaceUtils; +import org.exoplatform.social.core.space.model.Space; +import org.exoplatform.social.core.space.spi.SpaceService; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.model.CategoryFilter; +import io.meeds.social.category.model.CategorySearchFilter; +import io.meeds.social.category.model.CategoryTree; +import io.meeds.social.category.plugin.CategoryTranslationPlugin; +import io.meeds.social.category.storage.CategoryStorage; +import io.meeds.social.translation.service.TranslationService; + +import lombok.SneakyThrows; + +@Service +public class CategoryServiceImpl implements CategoryService { + + private static final String ADMINISTRATORS_GROUP = "/platform/administrators"; + + private static final long MAX_LIMIT = 100l; + + @Autowired + private IdentityManager identityManager; + + @Autowired + private TranslationService translationService; + + @Autowired + private CategoryStorage categoryStorage; + + @Autowired + private SpaceService spaceService; + + @Autowired + private UserACL userAcl; + + private long adminGroupOwnerId; + + @Override + public CategoryTree getCategoryTree(CategoryFilter filter, String username, Locale locale) { + long parentId = filter.getParentId(); + long ownerId = checkOwnerId(filter.getOwnerId(), filter.getParentId()); + long limit = checkLimit(filter.getLimit()); + Category category = parentId == 0 ? getRootCategory(ownerId) : getCategory(parentId); + if (category == null || !canAccess(category, username)) { + return null; + } + return buildCategoryTree(category, + username, + locale, + filter.getOffset(), + limit, + filter.getDepth(), + 0); + } + + @Override + public List findCategories(CategorySearchFilter filter, + String username, + Locale locale) { + long parentId = filter.getParentId(); + long ownerId = checkOwnerId(filter.getOwnerId(), filter.getParentId()); + long limit = checkLimit(filter.getLimit()); + Category category = parentId == 0 ? getRootCategory(ownerId) : getCategory(parentId); + if (category == null || !canAccess(category, username)) { + return Collections.emptyList(); + } + org.exoplatform.services.security.Identity userAclIdentity = userAcl.getUserIdentity(username); + if (userAclIdentity == null) { + return Collections.emptyList(); + } else { + Set groups = userAclIdentity.getGroups(); + List identityIds = groups.stream() + .map(groupId -> { + if (StringUtils.startsWith(groupId, SpaceUtils.SPACE_GROUP_PREFIX)) { + Space space = spaceService.getSpaceByGroupId(groupId); + if (space == null) { + return null; + } else { + Identity identity = identityManager.getOrCreateSpaceIdentity(space.getPrettyName()); + return Long.parseLong(identity.getId()); + } + } else { + Identity identity = identityManager.getOrCreateGroupIdentity(groupId); + return Long.parseLong(identity.getId()); + } + }) + .filter(Objects::nonNull) + .toList(); + filter = filter.clone(); + filter.setLimit(limit); + return categoryStorage.findCategories(filter, identityIds, locale); + } + } + + @Override + public Category getCategory(long categoryId) { + return categoryStorage.getCategory(categoryId); + } + + @Override + @SneakyThrows + public Category getRootCategory(long ownerId) { + Category rootCategory = categoryStorage.getRootCategory(ownerId); + if (rootCategory == null && ownerId == getAdminGroupIdentityId()) { + createCategory(new Category(0l, + 0l, + null, + null, + 0l, + ownerId, + Collections.emptyList(), + Arrays.asList(ownerId)), + userAcl.getSuperUser()); + } + return rootCategory; + } + + @Override + public Category createCategory(Category category, String username) throws ObjectNotFoundException, IllegalAccessException { + checkNotNull(category); + checkEmptyId(category); + checkOwnerId(category); + checkParentCreation(category); + checkCanEdit(category, username); + + Identity userIdentity = identityManager.getOrCreateUserIdentity(username); + category.setCreatorId(Long.parseLong(userIdentity.getId())); + return categoryStorage.createCategory(category); + } + + @Override + public Category updateCategory(Category category, String username) throws ObjectNotFoundException, IllegalAccessException { + checkNotNull(category); + checkNotEmptyId(category); + checkOwnerId(category); + checkParentUpdate(category); + Category existingCategory = checkCategoryExists(category.getId()); + if (existingCategory.getOwnerId() != category.getOwnerId()) { + throw new IllegalArgumentException("Category Owner Id is missing"); + } + checkCanEdit(category, username); + + category.setCreatorId(existingCategory.getCreatorId()); + return categoryStorage.updateCategory(category); + } + + @Override + public Category deleteCategory(long categoryId, String username) throws ObjectNotFoundException, IllegalAccessException { + Category category = checkCategoryExists(categoryId); + checkCanEdit(category, username); + return categoryStorage.deleteCategory(categoryId); + } + + @Override + public boolean canEdit(long categoryId, String username) { + Category category = getCategory(categoryId); + return canEdit(category, username); + } + + @Override + public boolean canEdit(Category category, String username) { + return category != null && isManagerOf(identityManager, + spaceService, + userAcl, + category.getOwnerId(), + username); + } + + @Override + public boolean canAccess(long categoryId, String username) { + return canAccess(getCategory(categoryId), username); + } + + @Override + public boolean canAccess(Category category, String username) { + if (category == null) { + return false; + } else if (CollectionUtils.isEmpty(category.getAccessPermissionIds())) { + return true; + } else { + org.exoplatform.services.security.Identity userAclIdentity = userAcl.getUserIdentity(username); + return userAcl.isAdministrator(userAclIdentity) + || category.getAccessPermissionIds() + .stream() + .anyMatch(id -> isMemberOf(identityManager, + spaceService, + userAcl, + id, + username)); + } + } + + private long getAdminGroupIdentityId() { + if (adminGroupOwnerId == 0) { + Identity adminGroupIdentity = identityManager.getOrCreateGroupIdentity(ADMINISTRATORS_GROUP); + adminGroupOwnerId = adminGroupIdentity == null ? 0l : Long.parseLong(adminGroupIdentity.getId()); + } + return adminGroupOwnerId; + } + + private long checkOwnerId(long ownerId, long parentId) { + if (ownerId == 0 && parentId == 0) { + ownerId = getAdminGroupIdentityId(); + if (ownerId == 0) { + throw new IllegalArgumentException("Either Parent Id or Owner Id has to be specified"); + } + } + return ownerId; + } + + private long checkLimit(long limit) { + if (limit > MAX_LIMIT) { + throw new IllegalArgumentException(String.format("Max categories to retrieve is %s, found %s", MAX_LIMIT, limit)); + } else if (limit <= 0) { + limit = MAX_LIMIT; + } + return limit; + } + + private void checkNotNull(Category category) { + if (category == null) { + throw new IllegalArgumentException("Category is mandatory"); + } + } + + private void checkEmptyId(Category category) { + if (category.getId() != 0) { + throw new IllegalArgumentException("Category id has to be empty"); + } + } + + private void checkNotEmptyId(Category category) { + if (category.getId() <= 0) { + throw new IllegalArgumentException("Category id is mandatory"); + } + } + + private void checkOwnerId(Category category) { + if (category.getOwnerId() <= 0) { + throw new IllegalArgumentException("Category owner identifier is mandatory"); + } + } + + private void checkParentCreation(Category category) throws ObjectNotFoundException { + if (category.getParentId() == 0) { + Category rootCategory = getRootCategory(category.getOwnerId()); + if (rootCategory != null) { + throw new IllegalArgumentException("Category root element already exists, thus can't recreate it"); + } + } else { + checkParentExists(category); + } + } + + private void checkParentUpdate(Category category) throws ObjectNotFoundException { + if (category.getParentId() == 0) { + Category rootCategory = getRootCategory(category.getOwnerId()); + if (rootCategory.getId() != category.getId()) { + throw new IllegalArgumentException("Category root element already exists, thus can't change it"); + } + } else { + checkParentExists(category); + } + } + + private void checkParentExists(Category category) throws ObjectNotFoundException { + Category parentCategory = getCategory(category.getParentId()); + if (parentCategory == null) { + throw new ObjectNotFoundException(String.format("Parent Category with id %s doesn't exist", category.getParentId())); + } + } + + private Category checkCategoryExists(long id) throws ObjectNotFoundException { + Category category = getCategory(id); + if (category == null) { + throw new ObjectNotFoundException(String.format("Can't update a not found Category with id %s", id)); + } + return category; + } + + private void checkCanEdit(Category category, String username) throws IllegalAccessException { + if (!canEdit(category, username)) { + throw new IllegalAccessException("Can't Update Category"); + } + } + + private CategoryTree buildCategoryTree(Category category, + String username, + Locale locale, + long offset, + long limit, + long depthLimit, + long depth) { + CategoryTree categoryTree = new CategoryTree(category); + String name = translationService.getTranslationLabelOrDefault(CategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + CategoryTranslationPlugin.NAME_FIELD, + locale); + categoryTree.setName(name); + if (depth < depthLimit) { + long categoryId = categoryTree.getId(); + List categories = buildSubCategories(categoryId, username, locale, offset, limit, depthLimit, depth); + categoryTree.setCategories(categories); + } + return categoryTree; + } + + private List buildSubCategories(long categoryId, + String username, + Locale locale, + long offset, + long limit, + long depthLimit, + long depth) { + List ids = categoryStorage.getCategoryChildrenIds(categoryId, offset, limit); + if (CollectionUtils.isNotEmpty(ids)) { + List categories = toCategories(ids, username, locale, offset, limit, depthLimit, depth + 1); + long offsetToFetch = offset; + long limitToFetch = Math.max(limit, 10); + boolean limitReached = categories.size() == ids.size() || ids.size() < limit; + while (!limitReached) { + offsetToFetch += limitToFetch; + ids = categoryStorage.getCategoryChildrenIds(categoryId, offset, limitToFetch); + List additionalCategories = toCategories(ids, + username, + locale, + offsetToFetch, + limitToFetch, + depthLimit, + depth + 1); + if (CollectionUtils.isNotEmpty(additionalCategories)) { + categories = new ArrayList<>(categories); + categories.addAll(additionalCategories.stream() + .limit(limit - categories.size()) + .toList()); + } + limitReached = categories.size() >= limit || ids.size() < limitToFetch; + } + return categories; + } else { + return Collections.emptyList(); + } + } + + private List toCategories(List categoryIds, + String username, + Locale locale, + long offset, + long limit, + long depthLimit, + long depth) { + return categoryIds.stream() + .map(categoryStorage::getCategory) + .filter(cat -> this.canAccess(cat, username)) + .map(cat -> buildCategoryTree(cat, + username, + locale, + offset, + limit, + depthLimit, + depth)) + .toList(); + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/storage/CategoryStorage.java b/component/core/src/main/java/io/meeds/social/category/storage/CategoryStorage.java new file mode 100644 index 00000000000..294b9f2b342 --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/storage/CategoryStorage.java @@ -0,0 +1,218 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.storage; + +import static io.meeds.social.category.service.CategoryService.EVENT_SOCIAL_CATEGORY_CREATED; +import static io.meeds.social.category.service.CategoryService.EVENT_SOCIAL_CATEGORY_DELETED; +import static io.meeds.social.category.service.CategoryService.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.listener.ListenerService; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.social.common.ObjectAlreadyExistsException; +import org.exoplatform.social.metadata.MetadataService; +import org.exoplatform.social.metadata.model.Metadata; +import org.exoplatform.social.metadata.model.MetadataItem; +import org.exoplatform.social.metadata.model.MetadataType; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.model.CategoryObject; +import io.meeds.social.category.model.CategorySearchFilter; +import io.meeds.social.category.storage.elasticsearch.CategorySearchConnector; + +@Component +@SuppressWarnings("removal") +public class CategoryStorage { + + private static final Log LOG = ExoLogger.getLogger(CategoryStorage.class); + + private static final String PROP_ACCESS_PERMISSIONS = "categoryAccessPermissions"; + + private static final String PROP_LINK_PERMISSIONS = "categoryLinkPermissions"; + + private static final String PROP_PARENT_ID = "categoryParentId"; + + private static final String PROP_OWNER_ROOT_ID = "categoryOwnerRootId"; + + private static final String PROP_ICON = "categoryIcon"; + + private static final MetadataType METADATA_TYPE = new MetadataType(54175l, "category"); + + @Autowired + private CategorySearchConnector searchConnector; + + @Autowired + private MetadataService metadataService; + + @Autowired + private ListenerService listenerService; + + public Category createCategory(Category category) { + Metadata metadata = toMetadata(category); + metadata = metadataService.createMetadata(metadata, category.getCreatorId()); + listenerService.broadcast(EVENT_SOCIAL_CATEGORY_CREATED, category, category.getCreatorId()); + return toCategory(metadata); + } + + @CacheEvict(cacheNames = "social.category", key = "#root.args[0].getId()") + public Category updateCategory(Category category) { + Metadata metadata = toMetadata(category); + metadata = metadataService.updateMetadata(metadata, category.getCreatorId()); + listenerService.broadcast(EVENT_SOCIAL_CATEGORY_UPDATED, category, category.getCreatorId()); + return toCategory(metadata); + } + + @CacheEvict(cacheNames = "social.category") + public Category deleteCategory(long id) { + List ids = getCategoryChildrenIds(id, 0, -1); + if (CollectionUtils.isNotEmpty(ids)) { + ids.forEach(this::deleteCategory); + } + Category category = getCategory(id); + if (category != null) { + metadataService.deleteMetadataById(category.getId()); + listenerService.broadcast(EVENT_SOCIAL_CATEGORY_DELETED, category, category.getCreatorId()); + } + return category; + } + + @Cacheable(cacheNames = "social.category") + public Category getCategory(long categoryId) { + Metadata metadata = metadataService.getMetadataById(categoryId); + return toCategory(metadata); + } + + @Cacheable(cacheNames = "social.categoryRootId") + public long getRootCategoryId(long ownerId) { + List metadatas = metadataService.getMetadatasByProperty(PROP_OWNER_ROOT_ID, String.valueOf(ownerId), 1); + return CollectionUtils.isEmpty(metadatas) ? 0 : metadatas.get(0).getId(); + } + + public Category getRootCategory(long ownerId) { + long rootId = getRootCategoryId(ownerId); + return rootId == 0 ? null : getCategory(rootId); + } + + public List getCategoryChildrenIds(long categoryId, long offset, long limit) { + return metadataService.getMetadataIdsByProperty(PROP_PARENT_ID, String.valueOf(categoryId), offset, limit); + } + + public List findCategories(CategorySearchFilter filter, List identityIds, Locale locale) { + List ids = searchConnector.search(filter, identityIds, locale); + return ids.stream().map(this::getCategory).toList(); + } + + public boolean isLinked(long categoryId, CategoryObject object) { + return getMetadataItem(categoryId, object) != null; + } + + public void link(long categoryId, CategoryObject object, long userIdentityId) { + Metadata metadata = metadataService.getMetadataById(categoryId); + try { + metadataService.createMetadataItem(object, metadata.key(), userIdentityId); + listenerService.broadcast(EVENT_SOCIAL_CATEGORY_ITEM_LINKED, object, categoryId); + } catch (ObjectAlreadyExistsException e) { + LOG.debug("Unable to link object {} to category {}", object, categoryId, e); + } + } + + public void unlink(long categoryId, CategoryObject object) { + MetadataItem metadataItem = getMetadataItem(categoryId, object); + if (metadataItem != null) { + try { + metadataService.deleteMetadataItem(metadataItem.getId(), metadataItem.getCreatorId()); + listenerService.broadcast(EVENT_SOCIAL_CATEGORY_ITEM_UNLINKED, object, categoryId); + } catch (ObjectNotFoundException e) { + LOG.debug("Unable to link object {} to category {}", object, categoryId, e); + } + } + } + + private Category toCategory(Metadata metadata) { + if (metadata == null) { + return null; + } + Category category = new Category(); + category.setId(metadata.getId()); + category.setOwnerId(metadata.getAudienceId()); + category.setCreatorId(metadata.getCreatorId()); + category.setIcon(metadata.getProperties().get(PROP_ICON)); + category.setParentId(Long.parseLong(metadata.getProperties().get(PROP_PARENT_ID))); + category.setAccessPermissionIds(toList(metadata.getProperties().get(PROP_ACCESS_PERMISSIONS))); + category.setLinkPermissionIds(toList(metadata.getProperties().get(PROP_LINK_PERMISSIONS))); + return category; + } + + private Metadata toMetadata(Category category) { + Map properties = new HashMap<>(); + if (category.getParentId() == 0) { + properties.put(PROP_OWNER_ROOT_ID, String.valueOf(category.getOwnerId())); + } + properties.put(PROP_PARENT_ID, String.valueOf(category.getParentId())); + properties.put(PROP_LINK_PERMISSIONS, toString(category.getLinkPermissionIds())); + properties.put(PROP_ACCESS_PERMISSIONS, String.valueOf(category.getAccessPermissionIds())); + properties.put(PROP_ICON, category.getIcon()); + return new Metadata(category.getId(), + METADATA_TYPE, + UUID.randomUUID().toString(), + category.getOwnerId(), + category.getCreatorId(), + System.currentTimeMillis(), + properties); + } + + public MetadataItem getMetadataItem(long categoryId, CategoryObject object) { + Metadata metadata = metadataService.getMetadataById(categoryId); + if (metadata == null) { + return null; + } else { + List items = metadataService.getMetadataItemsByMetadataAndObject(metadata.key(), object); + return CollectionUtils.isNotEmpty(items) ? items.get(0) : null; + } + } + + private String toString(List ids) { + return String.format("[%s]", ids == null ? "" : StringUtils.join(ids, ",")); + } + + private List toList(String ids) { + return StringUtils.isEmpty(ids) ? Collections.emptyList() : + Arrays.asList(ids.substring(1, ids.length() - 1).split(",")) + .stream() + .map(Long::parseLong) + .toList(); + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategoryIndexingConnector.java b/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategoryIndexingConnector.java new file mode 100644 index 00000000000..cbb00735042 --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategoryIndexingConnector.java @@ -0,0 +1,187 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.storage.elasticsearch; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import org.exoplatform.commons.search.domain.Document; +import org.exoplatform.commons.search.index.impl.ElasticIndexingServiceConnector; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.services.resources.LocaleConfig; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.social.core.search.DocumentWithMetadata; + +import io.meeds.social.category.model.Category; +import io.meeds.social.category.plugin.CategoryTranslationPlugin; +import io.meeds.social.category.storage.CategoryStorage; +import io.meeds.social.translation.model.TranslationField; +import io.meeds.social.translation.service.TranslationService; + +import lombok.SneakyThrows; + +public class CategoryIndexingConnector extends ElasticIndexingServiceConnector { + + public static final String TYPE = "category"; + + public static final String ES_MAPPING = """ + { + "properties" : { + "id" : {"type" : "keyword"}, + @name_mappings@, + "ownerId" : {"type" : "keyword"}, + "parentId" : {"type" : "keyword"}, + "creatorId" : {"type" : "keyword"}, + "icon" : {"type" : "keyword"}, + "accessPermissionIds" : {"type" : "keyword"}, + "linkPermissionIds" : {"type" : "keyword"}, + "lastUpdatedDate" : {"type" : "date", "format": "epoch_millis"} + } + } + """; + + public static final String NAME_MAPPING = """ + "@name@" : { + "type" : "text", + "analyzer": "ngram_analyzer", + "search_analyzer": "ngram_analyzer_search", + "index_options": "offsets", + "fields": { + "raw": { + "type": "keyword" + } + } + } + """; + + private CategoryStorage categoryStorage; + + private TranslationService translationService; + + private LocaleConfigService localeConfigService; + + public CategoryIndexingConnector(CategoryStorage categoryStorage, + TranslationService translationService, + LocaleConfigService localeConfigService, + InitParams initParams) { + super(initParams); + this.categoryStorage = categoryStorage; + this.translationService = translationService; + this.localeConfigService = localeConfigService; + } + + @Override + public String getConnectorName() { + return TYPE; + } + + @Override + public Document create(String id) { + if (StringUtils.isBlank(id)) { + throw new IllegalArgumentException("id is mandatory"); + } + + Category category = categoryStorage.getCategory(Long.parseLong(id)); + if (category == null) { + return null; + } + + Map fields = new HashMap<>(); + fields.put("id", String.valueOf(category.getId())); + fields.put("ownerId", String.valueOf(category.getOwnerId())); + fields.put("parentId", String.valueOf(category.getParentId())); + fields.put("creatorId", String.valueOf(category.getCreatorId())); + fields.put("icon", String.valueOf(category.getIcon())); + + DocumentWithMetadata document = new DocumentWithMetadata(); + document.setId(id); + document.setLastUpdatedDate(new Date()); + document.setFields(fields); + if (CollectionUtils.isNotEmpty(category.getAccessPermissionIds())) { + document.addListField("accessPermissionIds", category.getAccessPermissionIds().stream().map(String::valueOf).toList()); + } + if (CollectionUtils.isNotEmpty(category.getLinkPermissionIds())) { + document.addListField("linkPermissionIds", category.getLinkPermissionIds().stream().map(String::valueOf).toList()); + } + addTranslatedNames(category, document); + return document; + } + + @Override + public Document update(String id) { + return create(id); + } + + @Override + public List getAllIds(int offset, int limit) { + throw new UnsupportedOperationException(); + } + + @Override + public String getMapping() { + String nameMappings = localeConfigService.getLocalConfigs() + .stream() + .map(l -> NAME_MAPPING.replace("@name@", "name." + toLanguageTag(l))) + .collect(Collectors.joining(",\n")); + return ES_MAPPING.replace("@name_mappings@", nameMappings); + } + + @SneakyThrows + private void addTranslatedNames(Category category, DocumentWithMetadata document) { + TranslationField translationField = translationService.getTranslationField(CategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + CategoryTranslationPlugin.NAME_FIELD); + if (translationField != null && MapUtils.isNotEmpty(translationField.getLabels())) { + localeConfigService.getLocalConfigs() + .forEach(localeConfig -> document.addField("name." + toLanguageTag(localeConfig), + getTranslationLabelOrDefault(translationField, + localeConfig.getLocale()))); + } + } + + private String getTranslationLabelOrDefault(TranslationField translationField, + Locale locale) { + if (translationField != null && MapUtils.isNotEmpty(translationField.getLabels())) { + String label = translationField.getLabels().get(locale); + if (label == null) { + Locale defaultLocale = localeConfigService.getDefaultLocaleConfig().getLocale(); + label = translationField.getLabels().get(defaultLocale); + } + if (label == null) { + label = translationField.getLabels().values().iterator().next(); + } + return label; + } else { + return null; + } + } + + private String toLanguageTag(LocaleConfig localeConfig) { + return localeConfig.getLocale().toLanguageTag(); + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategorySearchConnector.java b/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategorySearchConnector.java new file mode 100644 index 00000000000..7ea79e608ac --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/storage/elasticsearch/CategorySearchConnector.java @@ -0,0 +1,198 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.storage.elasticsearch; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import org.exoplatform.commons.search.es.ElasticSearchException; +import org.exoplatform.commons.search.es.client.ElasticSearchingClient; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.container.xml.PropertiesParam; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; + +import io.meeds.social.category.model.CategorySearchFilter; + +public class CategorySearchConnector { + + private static final Log LOG = ExoLogger.getLogger(CategorySearchConnector.class); + + private static final String SEARCH_QUERY_TERM = """ + { + "from": "@offset@", + "size": "@limit@", + "query":{ + "bool":{ + "must":{ + "query_string":{ + "fields": ["@name_field@"], + "default_operator": "AND", + "query": "@term@~", + "fuzziness": 1, + "phrase_slop": 1 + } + }, + "filter":[ + @owner_id_query@ + @parent_id_query@ + @permissions_query@ + ] + } + }, + "fields": [], + "_source": false + } + """; + + public static final String OWNER_ID_QUERY = """ + { + "terms":{ + "ownerId": @ownerId@ + } + } + """; + + public static final String PARENT_ID_QUERY = """ + { + "terms":{ + "parentId": @parentId@ + } + } + """; + + public static final String PERMISSIONS_QUERY = """ + { + "terms":{ + "@permissions_field@": [@permissions@] + } + } + """; + + private static final String OFFSET_REPLACEMENT = "@offset@"; + + private static final String LIMIT_REPLACEMENT = "@limit@"; + + private static final String NAME_REPLACEMENT = "@name_field@"; + + private static final String TERM_REPLACEMENT = "@term@"; + + private static final String OWNER_ID_REPLACEMENT = "@ownerId@"; + + private static final String OWNER_ID_QUERY_REPLACEMENT = "@owner_id_query@"; + + private static final String PARENT_ID_REPLACEMENT = "@parentId@"; + + private static final String PARENT_ID_QUERY_REPLACEMENT = "@parent_id_query@"; + + private static final String PERMISSIONS_REPLACEMENT = "@permissions@"; + + private static final String PERMISSIONS_FIELD_REPLACEMENT = "@permissions_field@"; + + private static final String PERMISSIONS_QUERY_REPLACEMENT = "@permissions_query@"; + + private final ElasticSearchingClient client; + + private String index; + + public CategorySearchConnector(ElasticSearchingClient client, + InitParams initParams) { + this.client = client; + PropertiesParam param = initParams.getPropertiesParam("constructor.params"); + this.index = param.getProperty("index"); + } + + public List search(CategorySearchFilter filter, List identityIds, Locale locale) { + String esQuery = SEARCH_QUERY_TERM.replace(NAME_REPLACEMENT, "name." + locale.toLanguageTag()) + .replace(TERM_REPLACEMENT, filter.getTerm()) + .replace(OFFSET_REPLACEMENT, String.valueOf(filter.getOffset())) + .replace(LIMIT_REPLACEMENT, String.valueOf(filter.getLimit())); + String append = ""; + if (filter.getParentId() > 0) { + esQuery = esQuery.replace(PARENT_ID_QUERY_REPLACEMENT, + PARENT_ID_QUERY.replace(PARENT_ID_REPLACEMENT, String.valueOf(filter.getParentId()))); + append = ","; + } else if (filter.getOwnerId() > 0) { + esQuery = esQuery.replace(OWNER_ID_QUERY_REPLACEMENT, + OWNER_ID_QUERY.replace(OWNER_ID_REPLACEMENT, String.valueOf(filter.getParentId()))); + esQuery = esQuery.replace(PARENT_ID_QUERY_REPLACEMENT, ""); + append = ","; + } else { + esQuery = esQuery.replace(PARENT_ID_QUERY_REPLACEMENT, ""); + esQuery = esQuery.replace(OWNER_ID_QUERY_REPLACEMENT, ""); + } + if (CollectionUtils.isNotEmpty(identityIds)) { + esQuery = append + esQuery.replace(PERMISSIONS_QUERY_REPLACEMENT, + PERMISSIONS_QUERY.replace(PERMISSIONS_REPLACEMENT, StringUtils.join(identityIds, ",")) + .replace(PERMISSIONS_FIELD_REPLACEMENT, + filter.isLinkPermission() ? "linkPermissionIds" : + "accessPermissionIds")); + } else { + esQuery = esQuery.replace(PERMISSIONS_QUERY_REPLACEMENT, ""); + } + String jsonResponse = this.client.sendRequest(esQuery, this.index); + return buildResult(jsonResponse); + } + + @SuppressWarnings({ "rawtypes" }) + private List buildResult(String jsonResponse) { + JSONParser parser = new JSONParser(); + + Map json; + try { + json = (Map) parser.parse(jsonResponse); + } catch (ParseException e) { + throw new ElasticSearchException("Unable to parse JSON response", e); + } + + JSONObject jsonResult = (JSONObject) json.get("hits"); + if (jsonResult == null) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + JSONArray jsonHits = (JSONArray) jsonResult.get("hits"); + for (Object jsonHit : jsonHits) { + try { + JSONObject jsonHitObject = (JSONObject) jsonHit; + Long id = parseLong(jsonHitObject, "_id"); + results.add(id); + } catch (Exception e) { + LOG.warn("Error processing category search result item, ignore it from results", e); + } + } + return results; + } + + private Long parseLong(JSONObject hitSource, String key) { + String value = (String) hitSource.get(key); + return StringUtils.isBlank(value) ? null : Long.parseLong(value); + } + +} diff --git a/component/core/src/main/java/io/meeds/social/category/utils/Utils.java b/component/core/src/main/java/io/meeds/social/category/utils/Utils.java new file mode 100644 index 00000000000..c5fcecfd16f --- /dev/null +++ b/component/core/src/main/java/io/meeds/social/category/utils/Utils.java @@ -0,0 +1,99 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.category.utils; + +import org.apache.commons.lang3.StringUtils; + +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.GroupIdentityProvider; +import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; +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; + +public class Utils { + private Utils() { + // static methods + } + + public static boolean isMemberOf(IdentityManager identityManager, + SpaceService spaceService, + UserACL userAcl, + long identityId, + String username) { + org.exoplatform.services.security.Identity userAclIdentity = userAcl.getUserIdentity(username); + if (identityId == 0) { + return true; + } else if (userAclIdentity == null) { + return false; + } + Identity identity = identityManager.getIdentity(identityId); + if (identity == null) { + return false; + } + return switch (identity.getProviderId()) { + case SpaceIdentityProvider.NAME: { + Space space = spaceService.getSpaceByPrettyName(identity.getRemoteId()); + yield space != null && spaceService.canViewSpace(space, userAclIdentity.getUserId()); + } + case GroupIdentityProvider.NAME: { + yield userAclIdentity.isMemberOf(identity.getRemoteId()); + } + case OrganizationIdentityProvider.NAME: { + yield StringUtils.equals(identity.getRemoteId(), userAclIdentity.getUserId()); + } + default: + yield false; + }; + } + + public static boolean isManagerOf(IdentityManager identityManager, + SpaceService spaceService, + UserACL userAcl, + long identityId, + String username) { + org.exoplatform.services.security.Identity userAclIdentity = userAcl.getUserIdentity(username); + if (identityId == 0) { + return true; + } else if (userAclIdentity == null) { + return false; + } + Identity identity = identityManager.getIdentity(identityId); + if (identity == null) { + return false; + } + return switch (identity.getProviderId()) { + case SpaceIdentityProvider.NAME: { + Space space = spaceService.getSpaceByPrettyName(identity.getRemoteId()); + yield space != null && spaceService.canManageSpace(space, userAclIdentity.getUserId()); + } + case GroupIdentityProvider.NAME: { + yield userAclIdentity.isMemberOf(userAcl.getAdminMSType(), identity.getRemoteId()); + } + case OrganizationIdentityProvider.NAME: { + yield StringUtils.equals(identity.getRemoteId(), userAclIdentity.getUserId()); + } + default: + yield false; + }; + } + +} diff --git a/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/dao/jpa/MetadataDAO.java b/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/dao/jpa/MetadataDAO.java index ccd19638ece..f9131dfdafc 100644 --- a/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/dao/jpa/MetadataDAO.java +++ b/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/dao/jpa/MetadataDAO.java @@ -18,7 +18,6 @@ */ package org.exoplatform.social.core.jpa.storage.dao.jpa; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; @@ -31,7 +30,6 @@ import org.exoplatform.social.core.jpa.storage.entity.MetadataEntity; import jakarta.persistence.NoResultException; -import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; public class MetadataDAO extends GenericDAOJPAImpl { @@ -187,42 +185,20 @@ public List getMetadatas(long type, long limit) { } } - public List getMetadatasByProperty(String propertyKey, String propertyValue, long limit) { - try { - Query query = getMetadatasByPropertyQuery(propertyKey, propertyValue); - return getResultsFromQuery(query, limit); - } catch (NoResultException e) { - return Collections.emptyList(); - } - } - - private Query getMetadatasByPropertyQuery(String propertyKey, String propertyValue) { - StringBuilder queryStringBuilder = null; - queryStringBuilder = new StringBuilder("SELECT DISTINCT sm.metadata_id "); - queryStringBuilder.append(" FROM SOC_METADATAS sm \n"); - queryStringBuilder.append(" INNER JOIN SOC_METADATA_PROPERTIES sm_prop \n"); - queryStringBuilder.append(" ON sm.metadata_id = sm_prop.metadata_id \n"); - queryStringBuilder.append(" AND EXISTS ( SELECT sm_prop_tmp.metadata_id FROM SOC_METADATA_PROPERTIES as sm_prop_tmp \n"); - queryStringBuilder.append(" WHERE sm_prop_tmp.metadata_id = sm.metadata_id \n"); - queryStringBuilder.append(" AND sm_prop_tmp.name = '").append(propertyKey).append("' \n"); - queryStringBuilder.append(" AND sm_prop_tmp.value = '").append(propertyValue).append("' ) \n"); - return getEntityManager().createNativeQuery(queryStringBuilder.toString()); - } - - private List getResultsFromQuery(Query query, long limit) { + public List getMetadataIdsByProperty(String propertyKey, String propertyValue, long offset, long limit, boolean orderByName) { + TypedQuery query = getEntityManager().createNamedQuery(orderByName ? + "SocMetadataEntity.getMetadatasByPropertyOrderByName" : + "SocMetadataEntity.getMetadatasByPropertyOrderById", + Long.class); if (limit > 0) { query.setMaxResults((int) limit); } - List resultList = query.getResultList(); - List result = new ArrayList<>(); - for (Object object : resultList) { - String resultObject = String.valueOf(object); - if (resultObject == null) { - continue; - } - result.add(resultObject); + if (offset > 0) { + query.setFirstResult((int) offset); } - return result; + query.setParameter("propertyName", propertyKey); + query.setParameter("propertyValue", propertyValue); + return query.getResultList(); } } diff --git a/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/entity/MetadataEntity.java b/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/entity/MetadataEntity.java index 90c66a4bb3f..84aab47e439 100644 --- a/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/entity/MetadataEntity.java +++ b/component/core/src/main/java/org/exoplatform/social/core/jpa/storage/entity/MetadataEntity.java @@ -32,74 +32,89 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.MapKeyColumn; -import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; @Entity(name = "SocMetadataEntity") @Table(name = "SOC_METADATAS") -@NamedQueries( - { - @NamedQuery( - name = "SocMetadataEntity.findMetadata", - query = "SELECT sm FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.name = :name AND" - + " sm.audienceId = :audienceId" - ), - @NamedQuery( - name = "SocMetadataEntity.getMetadataNamesByAudiences", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.audienceId IN ( :audienceIds )" - + " ORDER BY sm.name ASC" - ), - @NamedQuery( - name = "SocMetadataEntity.getMetadataNamesByCreator", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.creatorId = :creatorId" - + " ORDER BY sm.name ASC" - ), - @NamedQuery( - name = "SocMetadataEntity.getMetadataNamesByUser", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.creatorId = :creatorId or sm.audienceId IN ( :audienceIds )" - + " ORDER BY sm.createdDate DESC, sm.name DESC" - ), - @NamedQuery( - name = "SocMetadataEntity.findMetadataNameByAudiencesAndQuery", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.audienceId IN ( :audienceIds ) AND" - + " LOWER(sm.name) LIKE :term" - + " ORDER BY sm.name ASC" - ), - @NamedQuery( - name = "SocMetadataEntity.findMetadataNameByCreatorAndQuery", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " sm.creatorId = :creatorId AND" - + " LOWER(sm.name) LIKE :term" - + " ORDER BY sm.name ASC" - ), - @NamedQuery( - name = "SocMetadataEntity.findMetadataNameByUserAndQuery", - query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" - + " sm.type = :type AND" - + " (sm.creatorId = :creatorId OR sm.audienceId IN ( :audienceIds )) AND" - + " LOWER(sm.name) LIKE :term" - + " ORDER BY sm.createdDate DESC, sm.name DESC" - ), - @NamedQuery( - name = "SocMetadataEntity.getMetadatas", - query = "SELECT sm FROM SocMetadataEntity sm WHERE" - + " sm.type = :type" - + " ORDER BY sm.name ASC" - ), - } +@NamedQuery( + name = "SocMetadataEntity.findMetadata", + query = "SELECT sm FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.name = :name AND" + + " sm.audienceId = :audienceId" +) +@NamedQuery( + name = "SocMetadataEntity.getMetadataNamesByAudiences", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.audienceId IN ( :audienceIds )" + + " ORDER BY sm.name ASC" +) +@NamedQuery( + name = "SocMetadataEntity.getMetadataNamesByCreator", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.creatorId = :creatorId" + + " ORDER BY sm.name ASC" +) +@NamedQuery( + name = "SocMetadataEntity.getMetadataNamesByUser", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.creatorId = :creatorId or sm.audienceId IN ( :audienceIds )" + + " ORDER BY sm.createdDate DESC, sm.name DESC" +) +@NamedQuery( + name = "SocMetadataEntity.findMetadataNameByAudiencesAndQuery", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.audienceId IN ( :audienceIds ) AND" + + " LOWER(sm.name) LIKE :term" + + " ORDER BY sm.name ASC" +) +@NamedQuery( + name = "SocMetadataEntity.findMetadataNameByCreatorAndQuery", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " sm.creatorId = :creatorId AND" + + " LOWER(sm.name) LIKE :term" + + " ORDER BY sm.name ASC" +) +@NamedQuery( + name = "SocMetadataEntity.findMetadataNameByUserAndQuery", + query = "SELECT sm.name FROM SocMetadataEntity sm WHERE" + + " sm.type = :type AND" + + " (sm.creatorId = :creatorId OR sm.audienceId IN ( :audienceIds )) AND" + + " LOWER(sm.name) LIKE :term" + + " ORDER BY sm.createdDate DESC, sm.name DESC" +) +@NamedQuery( + name = "SocMetadataEntity.getMetadatas", + query = "SELECT sm FROM SocMetadataEntity sm WHERE" + + " sm.type = :type" + + " ORDER BY sm.name ASC" +) +@NamedQuery( + name = "SocMetadataEntity.getMetadatasByPropertyOrderByName", + query = """ + SELECT sm.id FROM SocMetadataEntity sm + INNER JOIN sm.properties prop + ON key(prop) = :propertyName + AND value(prop) = :propertyValue + ORDER BY sm.name ASC + """ +) +@NamedQuery( + name = "SocMetadataEntity.getMetadatasByPropertyById", + query = """ + SELECT sm.id FROM SocMetadataEntity sm + INNER JOIN sm.properties prop + ON key(prop) = :propertyName + AND value(prop) = :propertyValue + ORDER BY sm.id DESC + """ ) public class MetadataEntity implements Serializable { diff --git a/component/core/src/main/java/org/exoplatform/social/core/metadata/MetadataServiceImpl.java b/component/core/src/main/java/org/exoplatform/social/core/metadata/MetadataServiceImpl.java index bf947c47a99..5e82e7c9965 100644 --- a/component/core/src/main/java/org/exoplatform/social/core/metadata/MetadataServiceImpl.java +++ b/component/core/src/main/java/org/exoplatform/social/core/metadata/MetadataServiceImpl.java @@ -122,6 +122,11 @@ public Metadata getMetadataByKey(MetadataKey metadataKey) { return metadataStorage.getMetadataByKey(metadataKey); } + @Override + public Metadata getMetadataById(long id) { + return metadataStorage.getMetadataById(id); + } + @Override public MetadataItem createMetadataItem(MetadataObject metadataObject, MetadataKey metadataKey, @@ -566,7 +571,13 @@ public List getMetadatas(String metadataTypeName, long limit) { @Override public List getMetadatasByProperty(String propertyKey, String propertyValue, long limit) { - return metadataStorage.getMetadatasByProperty(propertyKey, propertyValue, limit); + List ids = metadataStorage.getMetadataIdsByProperty(propertyKey, propertyValue, 0l, limit, true); + return ids.stream().map(this::getMetadataById).toList(); + } + + @Override + public List getMetadataIdsByProperty(String propertyKey, String propertyValue, long offset, long limit) { + return metadataStorage.getMetadataIdsByProperty(propertyKey, propertyValue, offset, limit, false); } @Override diff --git a/component/core/src/main/java/org/exoplatform/social/core/metadata/storage/MetadataStorage.java b/component/core/src/main/java/org/exoplatform/social/core/metadata/storage/MetadataStorage.java index 691c950bd32..a2424fa2c33 100644 --- a/component/core/src/main/java/org/exoplatform/social/core/metadata/storage/MetadataStorage.java +++ b/component/core/src/main/java/org/exoplatform/social/core/metadata/storage/MetadataStorage.java @@ -26,6 +26,10 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.exoplatform.commons.cache.future.FutureCache; +import org.exoplatform.commons.cache.future.FutureExoCache; +import org.exoplatform.services.cache.CacheService; +import org.exoplatform.services.cache.ExoCache; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.social.core.jpa.storage.dao.jpa.MetadataDAO; @@ -43,26 +47,47 @@ public class MetadataStorage { - private static final Log LOG = ExoLogger.getLogger(MetadataStorage.class); + private static final Log LOG = ExoLogger.getLogger(MetadataStorage.class); - private MetadataDAO metadataDAO; + private MetadataDAO metadataDAO; - private MetadataItemDAO metadataItemDAO; + private MetadataItemDAO metadataItemDAO; - private List metadataTypes = new ArrayList<>(); + private List metadataTypes = new ArrayList<>(); - public MetadataStorage(MetadataDAO metadataDAO, MetadataItemDAO metadataItemDAO) { + private ExoCache metadataCache; + + private FutureCache metadataFutureCache; + + private ExoCache metadataKeyCache; + + private FutureCache metadataKeyFutureCache; + + public MetadataStorage(MetadataDAO metadataDAO, + MetadataItemDAO metadataItemDAO, + CacheService cacheService) { this.metadataDAO = metadataDAO; this.metadataItemDAO = metadataItemDAO; + this.metadataCache = cacheService.getCacheInstance("sociam.metadata"); + this.metadataFutureCache = new FutureExoCache<>((c, k) -> fromEntity(metadataDAO.find(k)), metadataCache); + this.metadataKeyCache = cacheService.getCacheInstance("sociam.metadataKey"); + this.metadataKeyFutureCache = new FutureExoCache<>((c, k) -> { + String type = k.getType(); + MetadataType metadataType = getMetadataTypeWithCheck(type); + MetadataEntity metadataEntity = this.metadataDAO.findMetadata(metadataType.getId(), + k.getName(), + k.getAudienceId()); + return metadataEntity == null ? null : metadataEntity.getId(); + }, metadataKeyCache); } public Metadata getMetadataByKey(MetadataKey metadataKey) { - String type = metadataKey.getType(); - MetadataType metadataType = getMetadataTypeWithCheck(type); - MetadataEntity metadataEntity = this.metadataDAO.findMetadata(metadataType.getId(), - metadataKey.getName(), - metadataKey.getAudienceId()); - return fromEntity(metadataEntity); + Long id = this.metadataKeyFutureCache.get(null, metadataKey); + return id == null || id == 0 ? null : getMetadataById(id); + } + + public Metadata getMetadataById(long id) { + return this.metadataFutureCache.get(null, id); } public Metadata createMetadata(Metadata metadata) { @@ -72,17 +97,25 @@ public Metadata createMetadata(Metadata metadata) { } public Metadata updateMetadata(Metadata metadata) { - MetadataEntity metadataEntity = toEntity(metadata); - metadataEntity = this.metadataDAO.update(metadataEntity); - return fromEntity(metadataEntity); + try { + MetadataEntity metadataEntity = toEntity(metadata); + metadataEntity = this.metadataDAO.update(metadataEntity); + return fromEntity(metadataEntity); + } finally { + this.metadataCache.remove(metadata.getId()); + } } public Metadata deleteMetadataById(long id) { - MetadataEntity metadataEntity = this.metadataDAO.find(id); - if (metadataEntity != null) { - this.metadataDAO.delete(metadataEntity); + try { + MetadataEntity metadataEntity = this.metadataDAO.find(id); + if (metadataEntity != null) { + this.metadataDAO.delete(metadataEntity); + } + return fromEntity(metadataEntity); + } finally { + this.metadataCache.remove(id); } - return fromEntity(metadataEntity); } public int deleteMetadataItemsBySpaceId(long spaceId) { @@ -221,21 +254,20 @@ public List getMetadataItemsByMetadataNameAndTypeAndSpaceIds(Strin public List getMetadataItemsByFilter(MetadataFilter metadataFilter, long offset, long limit) { MetadataType metadataType = getMetadataTypeWithCheck(metadataFilter.getMetadataTypeName()); List metadataItemEntities = metadataItemDAO.getMetadataItemsByFilter(metadataFilter, - metadataType.getId(), - offset, - limit); + metadataType.getId(), + offset, + limit); if (CollectionUtils.isEmpty(metadataItemEntities)) { return Collections.emptyList(); } return metadataItemEntities.stream().map(this::fromEntity).toList(); } - public List getMetadataItemsByMetadataNameAndTypeAndObjectAndSpaceIds(String metadataName, - String metadataTypeName, - String objectType, - List spaceIds, - long offset, + String metadataTypeName, + String objectType, + List spaceIds, + long offset, long limit) { MetadataType metadataType = getMetadataTypeWithCheck(metadataTypeName); List metadataItemEntities = @@ -294,7 +326,10 @@ public List deleteByMetadataTypeAndSpaceIdAndCreatorId(long metada } public List deleteByMetadataTypeAndCreatorId(long metadataType, long userIdentityId) { - List metadataItemEntities = metadataItemDAO.getMetadataItemsByMetadataTypeAndCreator(metadataType, userIdentityId, 0, -1); + List metadataItemEntities = metadataItemDAO.getMetadataItemsByMetadataTypeAndCreator(metadataType, + userIdentityId, + 0, + -1); for (MetadataItemEntity metadataItemEntity : metadataItemEntities) { deleteMetadataItemById(metadataItemEntity.getId()); } @@ -364,8 +399,8 @@ public List getMetadataItemsByMetadataAndObject(long metadataId, M public List getMetadataItemsByMetadataTypeAndObject(long metadataType, MetadataObject object) { List metadataItemEntities = metadataItemDAO.getMetadataItemsByMetadataTypeAndObject(metadataType, - object.getType(), - object.getId()); + object.getType(), + object.getId()); if (CollectionUtils.isEmpty(metadataItemEntities)) { return Collections.emptyList(); } @@ -402,18 +437,8 @@ public List getMetadatas(String metadataTypeName, long limit) { return metadatasEntities.stream().map(this::fromEntity).toList(); } - public List getMetadatasByProperty(String propertyKey, String propertyValue, long limit) { - List metadatasEntitiesIds = metadataDAO.getMetadatasByProperty(propertyKey, propertyValue, limit); - List metadatasEntities = new ArrayList<>(); - if (metadatasEntitiesIds != null && !metadatasEntitiesIds.isEmpty()) { - for (String id : metadatasEntitiesIds) { - MetadataEntity metadataEntity = this.metadataDAO.find(Long.parseLong(id)); - if (metadataEntity != null) { - metadatasEntities.add(metadataEntity); - } - } - } - return metadatasEntities.stream().map(this::fromEntity).toList(); + public List getMetadataIdsByProperty(String propertyKey, String propertyValue, long offset, long limit, boolean orderByName) { + return metadataDAO.getMetadataIdsByProperty(propertyKey, propertyValue, offset, limit, orderByName); } public MetadataType getMetadataType(String name) { @@ -423,6 +448,11 @@ public MetadataType getMetadataType(String name) { .orElse(null); } + public void clearCaches() { + metadataCache.clearCache(); + metadataKeyCache.clearCache(); + } + public List getMetadataTypes() { return Collections.unmodifiableList(metadataTypes); } diff --git a/component/core/src/test/java/org/exoplatform/social/core/listeners/ActivityTagMetadataListenerTest.java b/component/core/src/test/java/org/exoplatform/social/core/listeners/ActivityTagMetadataListenerTest.java index 933d244f513..17bc4e583ce 100644 --- a/component/core/src/test/java/org/exoplatform/social/core/listeners/ActivityTagMetadataListenerTest.java +++ b/component/core/src/test/java/org/exoplatform/social/core/listeners/ActivityTagMetadataListenerTest.java @@ -29,6 +29,7 @@ import org.exoplatform.social.core.jpa.storage.dao.jpa.MetadataDAO; import org.exoplatform.social.core.manager.ActivityManager; import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.metadata.storage.MetadataStorage; import org.exoplatform.social.core.test.AbstractCoreTest; import org.exoplatform.social.metadata.MetadataService; import org.exoplatform.social.metadata.model.Metadata; @@ -71,6 +72,7 @@ public void setUp() throws Exception { johnIdentity = identityManager.getOrCreateUserIdentity("john"); maryIdentity = identityManager.getOrCreateUserIdentity("mary"); tearDownActivityList = new ArrayList<>(); + getContainer().getComponentInstanceOfType(MetadataStorage.class).clearCaches(); } @Override diff --git a/component/core/src/test/java/org/exoplatform/social/metadata/MetadataServiceTest.java b/component/core/src/test/java/org/exoplatform/social/metadata/MetadataServiceTest.java index ca36d9afd29..bf13dadb033 100644 --- a/component/core/src/test/java/org/exoplatform/social/metadata/MetadataServiceTest.java +++ b/component/core/src/test/java/org/exoplatform/social/metadata/MetadataServiceTest.java @@ -37,6 +37,7 @@ import org.exoplatform.social.common.ObjectAlreadyExistsException; import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.jpa.storage.dao.jpa.MetadataDAO; +import org.exoplatform.social.core.metadata.storage.MetadataStorage; import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.test.AbstractCoreTest; import org.exoplatform.social.metadata.model.Metadata; @@ -74,6 +75,7 @@ public void setUp() throws Exception { super.setUp(); metadataService = getContainer().getComponentInstanceOfType(MetadataService.class); metadataDAO = getContainer().getComponentInstanceOfType(MetadataDAO.class); + getContainer().getComponentInstanceOfType(MetadataStorage.class).clearCaches(); userMetadataType = new MetadataType(1000, "user"); spaceMetadataType = new MetadataType(2000, "space"); diff --git a/component/core/src/test/java/org/exoplatform/social/metadata/favorite/FavoriteServiceTest.java b/component/core/src/test/java/org/exoplatform/social/metadata/favorite/FavoriteServiceTest.java index 3a3ed49aee9..2895b612192 100644 --- a/component/core/src/test/java/org/exoplatform/social/metadata/favorite/FavoriteServiceTest.java +++ b/component/core/src/test/java/org/exoplatform/social/metadata/favorite/FavoriteServiceTest.java @@ -29,6 +29,7 @@ import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.jpa.storage.dao.jpa.MetadataDAO; import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.metadata.storage.MetadataStorage; import org.exoplatform.social.core.test.AbstractCoreTest; import org.exoplatform.social.metadata.MetadataService; import org.exoplatform.social.metadata.MetadataTypePlugin; @@ -60,6 +61,7 @@ public void setUp() throws Exception { metadataService = getContainer().getComponentInstanceOfType(MetadataService.class); favoriteService = getContainer().getComponentInstanceOfType(FavoriteService.class); metadataDAO = getContainer().getComponentInstanceOfType(MetadataDAO.class); + getContainer().getComponentInstanceOfType(MetadataStorage.class).clearCaches(); favoriteMetadataType = new MetadataType(1, "favorites"); userMetadataType = new MetadataType(2, "user"); diff --git a/component/core/src/test/java/org/exoplatform/social/metadata/tag/TagServiceTest.java b/component/core/src/test/java/org/exoplatform/social/metadata/tag/TagServiceTest.java index 29f4ab027dc..13e901736b2 100644 --- a/component/core/src/test/java/org/exoplatform/social/metadata/tag/TagServiceTest.java +++ b/component/core/src/test/java/org/exoplatform/social/metadata/tag/TagServiceTest.java @@ -31,6 +31,7 @@ import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; import org.exoplatform.social.core.jpa.storage.dao.jpa.MetadataDAO; +import org.exoplatform.social.core.metadata.storage.MetadataStorage; import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.test.AbstractCoreTest; import org.exoplatform.social.metadata.MetadataService; @@ -66,6 +67,8 @@ public void setUp() throws Exception { metadataService = getContainer().getComponentInstanceOfType(MetadataService.class); tagService = getContainer().getComponentInstanceOfType(TagService.class); metadataDAO = getContainer().getComponentInstanceOfType(MetadataDAO.class); + getContainer().getComponentInstanceOfType(MetadataStorage.class).clearCaches(); + johnIdentity = identityManager.getOrCreateUserIdentity("john"); tearDownSpaceList = new ArrayList<>();