From ef8f94522b34686662113acdba6bcee4fcccdf12 Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Tue, 28 May 2024 06:27:07 +0100 Subject: [PATCH] feat: Move Application Registry from Portal and implement Portlet Instances - MEED-6903 - Meeds-io/MIPs#139 (#79) This change will move the definition of JPA entity and Table of ApplicationRegistry renamed to Portlet Instances. In addition, this will implement all needed Backend features for MIPs#139 with notion of Categories, Portlets and instances defined with portlet preferences. --- .../dao/PortletInstanceCategoryDAO.java | 26 + .../meeds/layout/dao/PortletInstanceDAO.java | 31 + .../entity/PortletInstanceCategoryEntity.java | 63 +++ .../layout/entity/PortletInstanceEntity.java | 78 +++ .../meeds/layout/model/PortletDescriptor.java | 46 ++ .../meeds/layout/model/PortletInstance.java | 56 ++ .../layout/model/PortletInstanceCategory.java | 45 ++ .../PortletInstanceCategoryDescriptor.java | 45 ++ ...PortletInstanceCategoryDescriptorList.java | 34 ++ .../model/PortletInstanceDescriptor.java | 53 ++ .../model/PortletInstanceDescriptorList.java | 34 ++ .../model/PortletInstancePreference.java | 34 ++ .../PageTemplateAttachmentPlugin.java | 16 +- .../PortletInstanceAttachmentPlugin.java | 93 +++ .../PageTemplateTranslationPlugin.java | 16 +- ...tletInstanceCategoryTranslationPlugin.java | 94 +++ .../PortletInstanceTranslationPlugin.java | 94 +++ .../meeds/layout/rest/PageTemplateRest.java | 4 +- .../rest/PortletInstanceCategoryRest.java | 140 +++++ .../layout/rest/PortletInstanceRest.java | 141 +++++ .../io/meeds/layout/rest/PortletRest.java | 61 ++ .../meeds/layout/rest/model/LayoutModel.java | 4 +- .../layout/service/LayoutAclService.java | 21 + .../service/NavigationLayoutService.java | 2 +- .../layout/service/PageTemplateService.java | 4 +- .../service/PortletInstanceService.java | 280 +++++++++ .../meeds/layout/service/PortletService.java | 50 ++ .../LayoutTranslationImportService.java | 148 +++++ .../PageTemplateImportService.java | 171 ++---- .../PortletInstanceImportService.java | 369 ++++++++++++ .../PortletInstanceCategoryStorage.java | 78 +++ .../storage/PortletInstanceStorage.java | 92 +++ .../meeds/layout/storage/PortletStorage.java | 114 ++++ .../io/meeds/layout/util/EntityMapper.java | 110 ++++ .../layout-rdbms.db.changelog-1.0.0.xml | 478 +++++++++++++++- .../src/main/resources/jpa-entities.idx | 2 + .../portlet-instance-categories.json | 60 ++ .../src/main/resources/portlet-instances.json | 304 ++++++++++ .../PageTemplateAttachmentPluginTest.java | 12 +- .../PortletInstanceAttachmentPluginTest.java | 117 ++++ .../PageTemplateTranslationPluginTest.java | 14 +- ...InstanceCategoryTranslationPluginTest.java | 110 ++++ .../PortletInstanceTranslationPluginTest.java | 110 ++++ .../layout/rest/PageTemplateRestTest.java | 2 +- .../rest/PortletInstanceCategoryRestTest.java | 264 +++++++++ .../layout/rest/PortletInstanceRestTest.java | 265 +++++++++ .../io/meeds/layout/rest/PortletRestTest.java | 149 +++++ .../layout/service/LayoutAclServiceTest.java | 8 +- .../service/NavigationLayoutServiceTest.java | 2 +- .../layout/service/PageLayoutServiceTest.java | 24 +- .../service/PageTemplateServiceTest.java | 14 +- .../service/PortletInstanceServiceTest.java | 533 ++++++++++++++++++ .../layout/service/SiteLayoutServiceTest.java | 2 +- .../PageTemplateImportServiceTest.java | 34 +- .../PortletInstanceImportServiceTest.java | 74 +++ .../storage/PageTemplateStorageTest.java | 4 +- .../PortletInstanceCategoryStorageTest.java | 142 +++++ .../storage/PortletInstanceStorageTest.java | 145 +++++ .../layout/storage/PortletStorageTest.java | 152 +++++ .../resources/conf/portal/configuration.xml | 42 ++ .../locale/portlet/LayoutEditor_en.properties | 39 ++ .../src/main/webapp/WEB-INF/portlet.xml | 4 +- .../webapp/images/portlets/DefaultPortlet.png | Bin 0 -> 3643 bytes .../components/Illustration.vue | 3 +- .../js/PortletInstanceCategoryService.js | 87 +++ .../js/PortletInstanceService.js | 87 +++ .../js/PortletService.js | 27 +- .../main.js} | 21 +- .../vue-app/common-portlets/services.js | 38 ++ .../vue-app/common/js/PageTemplateService.js | 10 +- .../main/webapp/vue-app/common/services.js | 13 - .../content/common/ApplicationCard.vue | 28 +- .../common/ApplicationCategoryCard.vue | 24 +- .../drawer/AddApplicationDrawer.vue | 14 +- .../main/webapp/vue-app/layout-editor/main.js | 5 +- .../components/PortletsManagement.vue | 15 +- .../portlets/components/header/Toolbar.vue | 6 +- .../components/instances/Categories.vue | 62 ++ .../components/instances/CategoryItem.vue | 38 ++ .../components/instances/CategoryMenu.vue | 125 ++++ .../portlets/components/instances/Item.vue | 6 +- .../portlets/components/instances/List.vue | 10 +- .../portlets/components/instances/Main.vue | 22 + .../portlets/components/instances/Menu.vue | 2 +- .../portlets/components/portlets/Item.vue | 28 +- .../portlets/components/portlets/List.vue | 43 -- .../webapp/vue-app/portlets/initComponents.js | 11 + .../src/main/webapp/vue-app/portlets/main.js | 1 + 88 files changed, 6131 insertions(+), 348 deletions(-) create mode 100644 layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceCategoryDAO.java create mode 100644 layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceDAO.java create mode 100644 layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceCategoryEntity.java create mode 100644 layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceEntity.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletDescriptor.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstance.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategory.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptor.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptorList.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptor.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptorList.java create mode 100644 layout-service/src/main/java/io/meeds/layout/model/PortletInstancePreference.java rename layout-service/src/main/java/io/meeds/layout/plugin/{ => attachment}/PageTemplateAttachmentPlugin.java (82%) create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPlugin.java rename layout-service/src/main/java/io/meeds/layout/plugin/{ => translation}/PageTemplateTranslationPlugin.java (83%) create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java create mode 100644 layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java create mode 100644 layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/PortletService.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/injection/LayoutTranslationImportService.java rename layout-service/src/main/java/io/meeds/layout/service/{ => injection}/PageTemplateImportService.java (55%) create mode 100644 layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java create mode 100644 layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceCategoryStorage.java create mode 100644 layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceStorage.java create mode 100644 layout-service/src/main/java/io/meeds/layout/storage/PortletStorage.java create mode 100644 layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java create mode 100644 layout-service/src/main/resources/portlet-instance-categories.json create mode 100644 layout-service/src/main/resources/portlet-instances.json rename layout-service/src/test/java/io/meeds/layout/plugin/{ => attachment}/PageTemplateAttachmentPluginTest.java (89%) create mode 100644 layout-service/src/test/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPluginTest.java rename layout-service/src/test/java/io/meeds/layout/plugin/{ => translation}/PageTemplateTranslationPluginTest.java (86%) create mode 100644 layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPluginTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPluginTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/service/PortletInstanceServiceTest.java rename layout-service/src/test/java/io/meeds/layout/service/{ => injection}/PageTemplateImportServiceTest.java (68%) create mode 100644 layout-service/src/test/java/io/meeds/layout/service/injection/PortletInstanceImportServiceTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceCategoryStorageTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceStorageTest.java create mode 100644 layout-service/src/test/java/io/meeds/layout/storage/PortletStorageTest.java create mode 100644 layout-service/src/test/resources/conf/portal/configuration.xml create mode 100644 layout-webapp/src/main/webapp/images/portlets/DefaultPortlet.png create mode 100644 layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceCategoryService.js create mode 100644 layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js rename layout-webapp/src/main/webapp/vue-app/{common => common-portlets}/js/PortletService.js (76%) rename layout-webapp/src/main/webapp/vue-app/{common/js/PortletInstanceService.js => common-portlets/main.js} (71%) create mode 100644 layout-webapp/src/main/webapp/vue-app/common-portlets/services.js create mode 100644 layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Categories.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryItem.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryMenu.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Main.vue diff --git a/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceCategoryDAO.java b/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceCategoryDAO.java new file mode 100644 index 000000000..cae7c52a3 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceCategoryDAO.java @@ -0,0 +1,26 @@ +/** + * 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.layout.dao; + +import org.springframework.data.jpa.repository.JpaRepository; + +import io.meeds.layout.entity.PortletInstanceCategoryEntity; + +public interface PortletInstanceCategoryDAO extends JpaRepository { +} diff --git a/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceDAO.java b/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceDAO.java new file mode 100644 index 000000000..d3ed9c5b3 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/dao/PortletInstanceDAO.java @@ -0,0 +1,31 @@ +/** + * 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.layout.dao; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import io.meeds.layout.entity.PortletInstanceEntity; + +public interface PortletInstanceDAO extends JpaRepository { + + List findByCategoryId(long categoryId); + +} diff --git a/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceCategoryEntity.java b/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceCategoryEntity.java new file mode 100644 index 000000000..6083a2041 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceCategoryEntity.java @@ -0,0 +1,63 @@ +/** + * 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.layout.entity; + +import java.io.Serializable; +import java.util.List; + +import org.exoplatform.commons.utils.StringListConverter; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity(name = "LayoutApplicationCategory") +@Table(name = "PORTAL_APP_CATEGORIES") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceCategoryEntity implements Serializable { + + private static final long serialVersionUID = 8772040309317091459L; + + @Id + @SequenceGenerator(name = "SEQ_GTN_APPLICATION_CAT_ID", sequenceName = "SEQ_GTN_APPLICATION_CAT_ID", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_GTN_APPLICATION_CAT_ID") + @Column(name = "ID") + private Long id; + + @Convert(converter = StringListConverter.class) + @Column(name = "PERMISSIONS", nullable = false) + private List permissions; + + @Column(name = "ICON") + private String icon; + + @Column(name = "IS_SYSTEM") + private boolean system; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceEntity.java b/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceEntity.java new file mode 100644 index 000000000..c6bc6cc2b --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/entity/PortletInstanceEntity.java @@ -0,0 +1,78 @@ +/** + * 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.layout.entity; + +import java.io.Serializable; +import java.util.List; + +import org.exoplatform.commons.utils.StringListConverter; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity(name = "LayoutApplication") +@Table(name = "PORTAL_APPLICATIONS") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceEntity implements Serializable { + + private static final long serialVersionUID = 4955770436068594917L; + + @Id + @SequenceGenerator(name = "SEQ_GTN_APPLICATION_ID", sequenceName = "SEQ_GTN_APPLICATION_ID", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_GTN_APPLICATION_ID") + @Column(name = "ID") + private Long id; + + @Column(name = "CATEGORY_ID") + private long categoryId; + + @Column(name = "CONTENT_ID") + private String contentId; + + @Convert(converter = StringListConverter.class) + @Column(name = "PERMISSIONS") + private List permissions; + + @Column(name = "PREFERENCES") + private String preferences; + + @Column(name = "IS_SYSTEM") + private boolean system; + + @Column(name = "IS_SPACE_APPLICATION") + private boolean spaceApplication; + + @Column(name = "IS_DISABLED") + private boolean disabled; + + @Column(name = "FOOTPRINT") + private long footprint; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletDescriptor.java b/layout-service/src/main/java/io/meeds/layout/model/PortletDescriptor.java new file mode 100644 index 000000000..2de82c245 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletDescriptor.java @@ -0,0 +1,46 @@ +/** + * 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.layout.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PortletDescriptor { + + private String name; + + private String description; + + private String applicationName; + + private String portletName; + + private List supportedModes; + + public String getContentId() { + return applicationName + "/" + portletName; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstance.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstance.java new file mode 100644 index 000000000..c208d29ad --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstance.java @@ -0,0 +1,56 @@ +/** + * 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.layout.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PortletInstance { + + private long id; + + private String name; + + private String description; + + private long categoryId; + + private String contentId; + + private List preferences; + + private long illustrationId; + + private List permissions; + + private List supportedModes; + + private boolean system; + + private boolean disabled; + + private boolean spaceApplication; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategory.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategory.java new file mode 100644 index 000000000..0b54e24d0 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategory.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.layout.model; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PortletInstanceCategory { + + private long id; + + private String name; + + private String description; + + private String icon; + + private boolean system; + + private List permissions = new ArrayList<>(); + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptor.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptor.java new file mode 100644 index 000000000..b9093fb85 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptor.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.layout.model; + +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceCategoryDescriptor { + + private String nameId; + + private Map names; + + private Map descriptions; + + private String icon; + + private List permissions; + + private boolean system; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptorList.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptorList.java new file mode 100644 index 000000000..556f005ef --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceCategoryDescriptorList.java @@ -0,0 +1,34 @@ +/** + * 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.layout.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceCategoryDescriptorList { + + private List descriptors; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptor.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptor.java new file mode 100644 index 000000000..3ca26df36 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptor.java @@ -0,0 +1,53 @@ +/** + * 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.layout.model; + +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceDescriptor { + + private String nameId; + + private String portletName; + + private List preferences; + + private List permissions; + + private Map names; + + private Map descriptions; + + private String categoryNameId; + + private boolean system; + + private boolean spaceApplication; + + private String illustrationPath; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptorList.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptorList.java new file mode 100644 index 000000000..8adf9167b --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstanceDescriptorList.java @@ -0,0 +1,34 @@ +/** + * 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.layout.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstanceDescriptorList { + + private List descriptors; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/model/PortletInstancePreference.java b/layout-service/src/main/java/io/meeds/layout/model/PortletInstancePreference.java new file mode 100644 index 000000000..396124a0d --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/model/PortletInstancePreference.java @@ -0,0 +1,34 @@ +/** + * 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.layout.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PortletInstancePreference { + + private String name; + + private String value; + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateAttachmentPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPlugin.java similarity index 82% rename from layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateAttachmentPlugin.java rename to layout-service/src/main/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPlugin.java index 5966ca114..91bfd702d 100644 --- a/layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateAttachmentPlugin.java +++ b/layout-service/src/main/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPlugin.java @@ -16,18 +16,24 @@ * 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.layout.plugin; +package io.meeds.layout.plugin.attachment; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.services.security.Identity; import org.exoplatform.social.attachment.AttachmentPlugin; +import org.exoplatform.social.attachment.AttachmentService; import io.meeds.layout.service.LayoutAclService; +import jakarta.annotation.PostConstruct; + @Component +@Order(Ordered.HIGHEST_PRECEDENCE) public class PageTemplateAttachmentPlugin extends AttachmentPlugin { public static final String OBJECT_TYPE = "pageTemplate"; @@ -35,6 +41,14 @@ public class PageTemplateAttachmentPlugin extends AttachmentPlugin { @Autowired private LayoutAclService layoutAclService; + @Autowired + private AttachmentService attachmentService; + + @PostConstruct + public void init() { + attachmentService.addPlugin(this); + } + @Override public String getObjectType() { return OBJECT_TYPE; diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPlugin.java new file mode 100644 index 000000000..af5d989c5 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPlugin.java @@ -0,0 +1,93 @@ +/** + * 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.layout.plugin.attachment; + +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentPlugin; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; + +import jakarta.annotation.PostConstruct; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class PortletInstanceAttachmentPlugin extends AttachmentPlugin { + + public static final String OBJECT_TYPE = "portletInstance"; + + @Autowired + private LayoutAclService layoutAclService; + + @Autowired + private PortletInstanceService portletInstanceService; + + @Autowired + private AttachmentService attachmentService; + + @PostConstruct + public void init() { + attachmentService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasEditPermission(Identity userIdentity, String entityId) throws ObjectNotFoundException { + return userIdentity != null + && layoutAclService.isAdministrator(userIdentity.getUserId()); + } + + @Override + public boolean hasAccessPermission(Identity userIdentity, String entityId) throws ObjectNotFoundException { + PortletInstance portletInstance = portletInstanceService.getPortletInstance(Long.parseLong(entityId)); + if (portletInstance == null) { + throw new ObjectNotFoundException("Portlet instance not found"); + } + List permissions = portletInstance.getPermissions(); + return CollectionUtils.isEmpty(permissions) + || (userIdentity != null + && permissions.stream().anyMatch(p -> layoutAclService.isMemberOf(userIdentity.getUserId(), p))); + } + + @Override + public long getAudienceId(String objectId) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(String objectId) throws ObjectNotFoundException { + return 0; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateTranslationPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPlugin.java similarity index 83% rename from layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateTranslationPlugin.java rename to layout-service/src/main/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPlugin.java index 4b18c8a87..98ab0956a 100644 --- a/layout-service/src/main/java/io/meeds/layout/plugin/PageTemplateTranslationPlugin.java +++ b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPlugin.java @@ -16,17 +16,23 @@ * 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.layout.plugin; +package io.meeds.layout.plugin.translation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.service.LayoutAclService; import io.meeds.social.translation.plugin.TranslationPlugin; +import io.meeds.social.translation.service.TranslationService; + +import jakarta.annotation.PostConstruct; @Component +@Order(Ordered.HIGHEST_PRECEDENCE) public class PageTemplateTranslationPlugin extends TranslationPlugin { public static final String OBJECT_TYPE = "pageTemplate"; @@ -38,6 +44,14 @@ public class PageTemplateTranslationPlugin extends TranslationPlugin { @Autowired private LayoutAclService layoutAclService; + @Autowired + private TranslationService translationService; + + @PostConstruct + public void init() { + translationService.addPlugin(this); + } + @Override public String getObjectType() { return OBJECT_TYPE; diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPlugin.java new file mode 100644 index 000000000..b2fbebc84 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPlugin.java @@ -0,0 +1,94 @@ +/** + * 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.layout.plugin.translation; + +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.social.translation.plugin.TranslationPlugin; +import io.meeds.social.translation.service.TranslationService; + +import jakarta.annotation.PostConstruct; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class PortletInstanceCategoryTranslationPlugin extends TranslationPlugin { + + public static final String OBJECT_TYPE = "portletInstanceCategory"; + + public static final String DESCRIPTION_FIELD_NAME = "description"; + + public static final String TITLE_FIELD_NAME = "title"; + + @Autowired + private LayoutAclService layoutAclService; + + @Autowired + private PortletInstanceService portletInstanceService; + + @Autowired + private TranslationService translationService; + + @PostConstruct + public void init() { + translationService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasEditPermission(long id, String username) throws ObjectNotFoundException { + return layoutAclService.isAdministrator(username); + } + + @Override + public boolean hasAccessPermission(long id, String username) throws ObjectNotFoundException { + PortletInstanceCategory category = portletInstanceService.getPortletInstanceCategory(id); + if (category == null) { + throw new ObjectNotFoundException("Portlet instance category not found"); + } + List permissions = category.getPermissions(); + return CollectionUtils.isEmpty(permissions) + || permissions.stream().anyMatch(p -> layoutAclService.isMemberOf(username, p)); + } + + @Override + public long getAudienceId(long templateId) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(long templateId) throws ObjectNotFoundException { + return 0; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPlugin.java new file mode 100644 index 000000000..39ffa1209 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPlugin.java @@ -0,0 +1,94 @@ +/** + * 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.layout.plugin.translation; + +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.social.translation.plugin.TranslationPlugin; +import io.meeds.social.translation.service.TranslationService; + +import jakarta.annotation.PostConstruct; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class PortletInstanceTranslationPlugin extends TranslationPlugin { + + public static final String OBJECT_TYPE = "portletInstance"; + + public static final String DESCRIPTION_FIELD_NAME = "description"; + + public static final String TITLE_FIELD_NAME = "title"; + + @Autowired + private LayoutAclService layoutAclService; + + @Autowired + private PortletInstanceService portletInstanceService; + + @Autowired + private TranslationService translationService; + + @PostConstruct + public void init() { + translationService.addPlugin(this); + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasEditPermission(long id, String username) throws ObjectNotFoundException { + return layoutAclService.isAdministrator(username); + } + + @Override + public boolean hasAccessPermission(long id, String username) throws ObjectNotFoundException { + PortletInstance portletInstance = portletInstanceService.getPortletInstance(id); + if (portletInstance == null) { + throw new ObjectNotFoundException("Portlet instance not found"); + } + List permissions = portletInstance.getPermissions(); + return CollectionUtils.isEmpty(permissions) + || permissions.stream().anyMatch(p -> layoutAclService.isMemberOf(username, p)); + } + + @Override + public long getAudienceId(long templateId) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(long templateId) throws ObjectNotFoundException { + return 0; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java index 556e6821d..dd41caf2d 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java @@ -46,8 +46,8 @@ import jakarta.servlet.http.HttpServletRequest; @RestController -@RequestMapping("pageTemplates") -@Tag(name = "pageTemplates", description = "Managing page templates") +@RequestMapping("/page/templates") +@Tag(name = "/layout/rest/page/templates", description = "Managing page templates") public class PageTemplateRest { @Autowired diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java new file mode 100644 index 000000000..bfaf1acf1 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java @@ -0,0 +1,140 @@ +/** + * 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.layout.rest; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.service.PortletInstanceService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/portlet/instance/categories") +@Tag(name = "/layout/rest/portlet/instance/categories", description = "Managing portlet instance categorys") +public class PortletInstanceCategoryRest { + + @Autowired + private PortletInstanceService portletInstanceService; + + @GetMapping + @Secured("users") + @Operation(summary = "Retrieve portlet instance categorys", method = "GET", + description = "This retrieves portlet instance categorys") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + public List getPortletInstanceCategorys(HttpServletRequest request) { + return portletInstanceService.getPortletInstanceCategories(request.getLocale(), true); + } + + @GetMapping("{id}") + @Secured("users") + @Operation(summary = "Retrieve a portlet instance category designated by its id", method = "GET", + description = "This will retrieve a portlet instance category designated by its id") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + public PortletInstanceCategory getPortletInstanceCategory( + HttpServletRequest request, + @Parameter(description = "Portlet instance category identifier") + @PathVariable("id") + long id) { + return portletInstanceService.getPortletInstanceCategory(id, request.getLocale(), true); + } + + @PostMapping + @Secured("users") + @Operation(summary = "Create a portlet instance category", method = "POST", + description = "This creates a new portlet instance category") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "portlet instance category created"), + @ApiResponse(responseCode = "400", description = "Invalid query input") }) + public PortletInstanceCategory createPortletInstanceCategory( + HttpServletRequest request, + @RequestBody + PortletInstanceCategory portletInstanceCategory) { + try { + return portletInstanceService.createPortletInstanceCategory(portletInstanceCategory, request.getRemoteUser()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @PutMapping("{id}") + @Secured("users") + @Operation(summary = "Update a portlet instance category", method = "PUT", + description = "This updates an existing portlet instance category") + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance category updated"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "404", description = "Object Not found") }) + public void updatePortletInstanceCategory( + HttpServletRequest request, + @Parameter(description = "Portlet instance category identifier") + @PathVariable("id") + long id, + @RequestBody + PortletInstanceCategory portletInstanceCategory) { + try { + portletInstanceCategory.setId(id); + portletInstanceService.updatePortletInstanceCategory(portletInstanceCategory, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @DeleteMapping("{id}") + @Secured("users") + @Operation(summary = "Deletes a portlet instance category", method = "DELETE", + description = "This deletes an existing portlet instance category") + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance category deleted"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "404", description = "Object Not found") }) + public void deletePortletInstanceCategory( + HttpServletRequest request, + @Parameter(description = "Portlet instance category identifier") + @PathVariable("id") + long id) { + try { + portletInstanceService.deletePortletInstanceCategory(id, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java new file mode 100644 index 000000000..40660f13f --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java @@ -0,0 +1,141 @@ +/** + * 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.layout.rest; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.PortletInstanceService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/portlet/instances") +@Tag(name = "/layout/rest/portlet/instances", description = "Managing portlet instances") +public class PortletInstanceRest { + + @Autowired + private PortletInstanceService portletInstanceService; + + @GetMapping + @Secured("users") + @Operation(summary = "Retrieve portlet instances", method = "GET", description = "This retrieves portlet instances") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + public List getPortletInstances( + HttpServletRequest request, + @Parameter(description = "Portlet instance category identifier") + @RequestParam(name = "categoryId", required = false, defaultValue = "0") + long categoryId) { + return portletInstanceService.getPortletInstances(categoryId, request.getLocale(), true); + } + + @GetMapping("{id}") + @Secured("users") + @Operation(summary = "Retrieve a portlet instance designated by its id", method = "GET", + description = "This will retrieve a portlet instance designated by its id") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + public PortletInstance getPortletInstance( + HttpServletRequest request, + @Parameter(description = "Portlet instance identifier") + @PathVariable("id") + long id) { + return portletInstanceService.getPortletInstance(id, request.getLocale(), true); + } + + @PostMapping + @Secured("users") + @Operation(summary = "Create a portlet instance", method = "POST", description = "This creates a new portlet instance") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "portlet instance created"), + @ApiResponse(responseCode = "400", description = "Invalid query input") }) + public PortletInstance createPortletInstance( + HttpServletRequest request, + @RequestBody + PortletInstance portletInstance) { + try { + return portletInstanceService.createPortletInstance(portletInstance, request.getRemoteUser()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @PutMapping("{id}") + @Secured("users") + @Operation(summary = "Update a portlet instance", method = "PUT", description = "This updates an existing portlet instance") + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance updated"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "404", description = "Object Not found") }) + public void updatePortletInstance( + HttpServletRequest request, + @Parameter(description = "Portlet instance identifier") + @PathVariable("id") + long id, + @RequestBody + PortletInstance portletInstance) { + try { + portletInstance.setId(id); + portletInstanceService.updatePortletInstance(portletInstance, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @DeleteMapping("{id}") + @Secured("users") + @Operation(summary = "Deletes a portlet instance", method = "DELETE", description = "This deletes an existing portlet instance") + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance deleted"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "404", description = "Object Not found") }) + public void deletePortletInstance( + HttpServletRequest request, + @Parameter(description = "Portlet instance identifier") + @PathVariable("id") + long id) { + try { + portletInstanceService.deletePortletInstance(id, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java new file mode 100644 index 000000000..bc1278ece --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java @@ -0,0 +1,61 @@ +/** + * 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.layout.rest; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.meeds.layout.model.PortletDescriptor; +import io.meeds.layout.service.PortletService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RestController +@RequestMapping("portlets") +@Tag(name = "portlets", description = "Retrieving available portlets") +public class PortletRest { + + @Autowired + private PortletService portletService; + + @GetMapping + @Secured({ + "administrators", + "web-contributors", + }) + @Operation( + summary = "Retrieve portlets", + method = "GET", + description = "This retrieves the list of available portlets in the platform") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) + public List getPortlets() { + return portletService.getPortlets(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java b/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java index b311716e3..5843cd112 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java @@ -175,7 +175,7 @@ private void init(ModelObject model) { // NOSONAR } else if (state instanceof TransientApplicationState transientState) { this.contentId = transientState.getContentId(); } else { - throw new IllegalStateException("Application should either has a persistent or transient state"); + throw new IllegalStateException("PortletInstance should either has a persistent or transient state"); } } } @@ -242,7 +242,7 @@ public static ModelObject toModelObject(LayoutModel layoutModel) { transientState.setOwnerType(layoutModel.getOwnerType()); state = transientState; } else { - throw new IllegalStateException("Application should either has a storageId or a contentId"); + throw new IllegalStateException("PortletInstance should either has a storageId or a contentId"); } application.setState(state); return application; diff --git a/layout-service/src/main/java/io/meeds/layout/service/LayoutAclService.java b/layout-service/src/main/java/io/meeds/layout/service/LayoutAclService.java index a7f843e40..5e91ee551 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/LayoutAclService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/LayoutAclService.java @@ -34,6 +34,7 @@ import org.exoplatform.services.security.Identity; import org.exoplatform.services.security.IdentityConstants; import org.exoplatform.services.security.IdentityRegistry; +import org.exoplatform.social.core.manager.IdentityManager; import jakarta.annotation.PostConstruct; import lombok.Setter; @@ -51,6 +52,9 @@ public class LayoutAclService { @Autowired private Authenticator authenticator; + @Autowired + private IdentityManager identityManager; + @Setter private IdentityRegistry identityRegistry; @@ -169,6 +173,16 @@ public boolean isAdministrator(String username) { } } + public boolean isMemberOf(String username, String expression) { + ConversationState currentConversationState = ConversationState.getCurrent(); + ConversationState.setCurrent(getConversationState(username)); + try { + return userAcl.hasPermission(expression); + } finally { + ConversationState.setCurrent(currentConversationState); + } + } + public String getAdministratorsGroup() { return userAcl.getAdminGroups(); } @@ -177,6 +191,13 @@ public ConversationState getSuperUserConversationState() { return new ConversationState(getUserIdentity(userAcl.getSuperUser())); } + public long getSuperUserIdentityId() { + org.exoplatform.social.core.identity.model.Identity userIdentity = + identityManager.getOrCreateUserIdentity(userAcl.getSuperUser()); + String id = userIdentity == null ? null : userIdentity.getId(); + return id == null ? 0 : Long.parseLong(id); + } + private ConversationState getConversationState(String username) { return new ConversationState(getUserIdentity(username)); } diff --git a/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java index 73a422de6..3d035de8f 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java @@ -362,7 +362,7 @@ private NodeState buildNodeState(String label, // NOSONAR private void saveNodeLabels(String nodeId, Map labels) { if (labels != null) { Map nodeLabels = new HashMap<>(); - labels.entrySet().forEach(label -> nodeLabels.put(new Locale(label.getKey()), new State(label.getValue(), null))); + labels.entrySet().forEach(label -> nodeLabels.put(Locale.forLanguageTag(label.getKey()), new State(label.getValue(), null))); descriptionService.setDescriptions(nodeId, nodeLabels); } else { descriptionService.setDescriptions(nodeId, Collections.emptyMap()); diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java index 782aa19dd..ff840e870 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java @@ -33,8 +33,8 @@ import org.exoplatform.social.attachment.AttachmentService; import io.meeds.layout.model.PageTemplate; -import io.meeds.layout.plugin.PageTemplateAttachmentPlugin; -import io.meeds.layout.plugin.PageTemplateTranslationPlugin; +import io.meeds.layout.plugin.attachment.PageTemplateAttachmentPlugin; +import io.meeds.layout.plugin.translation.PageTemplateTranslationPlugin; import io.meeds.layout.storage.PageTemplateStorage; import io.meeds.social.translation.model.TranslationField; import io.meeds.social.translation.service.TranslationService; diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java new file mode 100644 index 000000000..5653ca0bf --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java @@ -0,0 +1,280 @@ +/** + * 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.layout.service; + +import java.util.List; +import java.util.Locale; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.plugin.attachment.PortletInstanceAttachmentPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceCategoryTranslationPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceTranslationPlugin; +import io.meeds.layout.storage.PortletInstanceCategoryStorage; +import io.meeds.layout.storage.PortletInstanceStorage; +import io.meeds.social.translation.model.TranslationField; +import io.meeds.social.translation.service.TranslationService; + +@Service +public class PortletInstanceService { + + private static final Log LOG = ExoLogger.getLogger(PortletInstanceService.class); + + @Autowired + private LayoutAclService layoutAclService; + + @Autowired + private TranslationService translationService; + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private LocaleConfigService localeConfigService; + + @Autowired + private PortletInstanceCategoryStorage portletInstanceCategoryStorage; + + @Autowired + private PortletInstanceStorage portletInstanceStorage; + + public List getPortletInstances() { + return getPortletInstances(null, false); + } + + public List getPortletInstances(boolean expand) { + return getPortletInstances(null, expand); + } + + public List getPortletInstances(Locale locale, + boolean expand) { + return getPortletInstances(0, locale, expand); + } + + public List getPortletInstances(long categoryId, + Locale locale, + boolean expand) { + List portletInstances = categoryId < 1 ? portletInstanceStorage.getPortletInstances() : + portletInstanceStorage.getPortletInstances(categoryId); + if (expand) { + portletInstances.forEach(portletInstance -> computePortletInstanceAttributes(locale, portletInstance)); + } + return portletInstances; + } + + public List getPortletInstanceCategories() { + return getPortletInstanceCategories(null, false); + } + + public List getPortletInstanceCategories(Locale locale, boolean expand) { + List portletInstanceCategories = portletInstanceCategoryStorage.getPortletInstanceCategories(); + if (expand && locale != null) { + portletInstanceCategories.forEach(portletInstance -> computePortletInstanceCategoryAttributes(locale, portletInstance)); + } + return portletInstanceCategories; + } + + public PortletInstance getPortletInstance(long id, Locale locale, boolean expand) { + PortletInstance portletInstance = portletInstanceStorage.getPortletInstance(id); + if (expand && portletInstance != null) { + computePortletInstanceAttributes(locale, portletInstance); + } + return portletInstance; + } + + public PortletInstanceCategory getPortletInstanceCategory(long id) { + return portletInstanceCategoryStorage.getPortletInstanceCategory(id); + } + + public PortletInstanceCategory getPortletInstanceCategory(long id, Locale locale, boolean expand) { + PortletInstanceCategory portletInstanceCategory = portletInstanceCategoryStorage.getPortletInstanceCategory(id); + if (expand && portletInstanceCategory != null) { + computePortletInstanceCategoryAttributes(locale, portletInstanceCategory); + } + return portletInstanceCategory; + } + + public PortletInstance getPortletInstance(long id) { + return portletInstanceStorage.getPortletInstance(id); + } + + public PortletInstance createPortletInstance(PortletInstance portletInstance, String username) throws IllegalAccessException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to create a Portlet instance"); + } + return createPortletInstance(portletInstance); + } + + public PortletInstance createPortletInstance(PortletInstance portletInstance) { + return portletInstanceStorage.createPortletInstance(portletInstance); + } + + public PortletInstanceCategory createPortletInstanceCategory(PortletInstanceCategory portletInstanceCategory, + String username) throws IllegalAccessException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to create a Portlet instance Category"); + } + return createPortletInstanceCategory(portletInstanceCategory); + } + + public PortletInstanceCategory createPortletInstanceCategory(PortletInstanceCategory category) { + return portletInstanceCategoryStorage.createPortletInstanceCategory(category); + } + + public void deletePortletInstance(long id, String username) throws IllegalAccessException, ObjectNotFoundException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to delete a Portlet instance"); + } + PortletInstance portletInstance = getPortletInstance(id); + if (portletInstance == null) { + throw new ObjectNotFoundException("Portlet instance doesn't exist"); + } + if (portletInstance.isSystem()) { + throw new IllegalAccessException("Can't delete a system Portlet instance"); + } + deletePortletInstance(id); + } + + public void deletePortletInstance(long id) throws ObjectNotFoundException { + try { + attachmentService.deleteAttachments(PortletInstanceAttachmentPlugin.OBJECT_TYPE, String.valueOf(id)); + } catch (Exception e) { + LOG.debug("Error while deleting attachments of deleted Portlet instance", e); + } + try { + translationService.deleteTranslationLabels(PortletInstanceTranslationPlugin.OBJECT_TYPE, id); + } catch (ObjectNotFoundException e) { + LOG.debug("Error while deleting translation labels of deleted Portlet instance", e); + } + portletInstanceStorage.deletePortletInstance(id); + } + + public void deletePortletInstanceCategory(long id, String username) throws IllegalAccessException, ObjectNotFoundException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to create a Portlet instance Category"); + } + PortletInstanceCategory portletInstanceCategory = getPortletInstanceCategory(id); + if (portletInstanceCategory == null) { + throw new ObjectNotFoundException("Portlet instance Category doesn't exist"); + } + if (portletInstanceCategory.isSystem()) { + throw new IllegalAccessException("Can't delete a system Portlet instance Category"); + } + deletePortletInstanceCategory(id); + } + + public void deletePortletInstanceCategory(long id) throws ObjectNotFoundException { + try { + translationService.deleteTranslationLabels(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, id); + } catch (ObjectNotFoundException e) { + LOG.debug("Error while deleting translation labels of deleted Portlet instance Category", e); + } + portletInstanceCategoryStorage.deletePortletInstanceCategory(id); + } + + public PortletInstance updatePortletInstance(PortletInstance portletInstance, String username) throws ObjectNotFoundException, + IllegalAccessException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to update a Portlet instance"); + } + return updatePortletInstance(portletInstance); + } + + public PortletInstance updatePortletInstance(PortletInstance portletInstance) throws ObjectNotFoundException { + return portletInstanceStorage.updatePortletInstance(portletInstance); + } + + public PortletInstanceCategory updatePortletInstanceCategory(PortletInstanceCategory portletInstanceCategory, + String username) throws ObjectNotFoundException, + IllegalAccessException { + if (!layoutAclService.isAdministrator(username)) { + throw new IllegalAccessException("User isn't authorized to update a Portlet instance category"); + } + return updatePortletInstanceCategory(portletInstanceCategory); + } + + public PortletInstanceCategory updatePortletInstanceCategory(PortletInstanceCategory category) throws ObjectNotFoundException { + return portletInstanceCategoryStorage.updatePortletInstanceCategory(category); + } + + private void computePortletInstanceAttributes(Locale locale, PortletInstance portletInstance) { + portletInstance.setName(getLabel(PortletInstanceTranslationPlugin.OBJECT_TYPE, + portletInstance.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME, + locale)); + portletInstance.setDescription(getLabel(PortletInstanceTranslationPlugin.OBJECT_TYPE, + portletInstance.getId(), + PortletInstanceTranslationPlugin.DESCRIPTION_FIELD_NAME, + locale)); + List attachmentFileIds = attachmentService.getAttachmentFileIds(PortletInstanceAttachmentPlugin.OBJECT_TYPE, + String.valueOf(portletInstance.getId())); + if (CollectionUtils.isNotEmpty(attachmentFileIds)) { + portletInstance.setIllustrationId(Long.parseLong(attachmentFileIds.get(0))); + } + } + + private void computePortletInstanceCategoryAttributes(Locale locale, PortletInstanceCategory portletInstanceCategory) { + portletInstanceCategory.setName(getLabel(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + portletInstanceCategory.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME, + locale)); + portletInstanceCategory.setDescription(getLabel(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + portletInstanceCategory.getId(), + PortletInstanceCategoryTranslationPlugin.DESCRIPTION_FIELD_NAME, + locale)); + } + + private String getLabel(String objectType, long objectId, String fieldName, Locale locale) { + if (locale == null) { + locale = localeConfigService.getDefaultLocaleConfig().getLocale(); + } + try { + TranslationField translationField = translationService.getTranslationField(objectType, + objectId, + fieldName); + 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; + } + } catch (ObjectNotFoundException e) { + return null; + } + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletService.java new file mode 100644 index 000000000..5d743d08d --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletService.java @@ -0,0 +1,50 @@ +/** + * 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.layout.service; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import io.meeds.layout.model.PortletDescriptor; +import io.meeds.layout.storage.PortletStorage; + +@Service +public class PortletService { + + @Autowired + private PortletStorage portletStorage; + + /** + * @return a {@link List} of {@link PortletDescriptor} + */ + public List getPortlets() { + return portletStorage.getPortletDescriptors(); + } + + /** + * @param id + * @return {@link PortletDescriptor} corresponding to contentId or portletName + */ + public PortletDescriptor getPortlet(String id) { + return portletStorage.getPortletDescriptor(id); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/service/injection/LayoutTranslationImportService.java b/layout-service/src/main/java/io/meeds/layout/service/injection/LayoutTranslationImportService.java new file mode 100644 index 000000000..731e273d7 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/injection/LayoutTranslationImportService.java @@ -0,0 +1,148 @@ +/** + * 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.layout.service.injection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.container.PortalContainer; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.services.resources.ResourceBundleService; + +import io.meeds.social.translation.service.TranslationService; + +import lombok.SneakyThrows; + +@Component +public class LayoutTranslationImportService { + + private static final Log LOG = ExoLogger.getLogger(LayoutTranslationImportService.class); + + @Autowired + private TranslationService translationService; + + @Autowired + private LocaleConfigService localeConfigService; + + @Autowired + private ResourceBundleService resourceBundleService; + + private Map> postImportProcessors = new ConcurrentHashMap<>(); + + private Map bundles = new ConcurrentHashMap<>(); + + public void saveTranslationLabels(String objectType, + long objectId, + String fieldName, + Map labels) { + try { + translationService.deleteTranslationLabels(objectType, + objectId, + fieldName); + } catch (Exception e) { // NOSONAR + // Normal, when not exists + } + String defaultLabel = saveDefaultTranslationLabel(objectType, + objectId, + fieldName, + labels.get("en")); + // Make Heavy processing made at the end or import process + postImportProcessors.computeIfAbsent(objectType, k -> new ArrayList<>()) + .add(() -> saveTranslationLabelsForAllLanguages(objectType, + objectId, + fieldName, + labels, + defaultLabel)); + } + + public void postImport(String objectType) { + postImportProcessors.computeIfAbsent(objectType, k -> new ArrayList<>()).forEach(Runnable::run); + postImportProcessors.remove(objectType); + bundles.clear(); + } + + @SneakyThrows + private void saveTranslationLabelsForAllLanguages(String objectType, + long objectId, + String fieldName, + Map labels, + String defaultLabel) { + String i18nKey = labels.get("en"); + Map translations = new HashMap<>(); + localeConfigService.getLocalConfigs() + .stream() + .filter(config -> !StringUtils.equals(config.getLocale().toLanguageTag(), "ma")) + .forEach(config -> translations.put(config.getLocale(), + getI18NLabel(i18nKey, + config.getLocale(), + defaultLabel))); + translationService.saveTranslationLabels(objectType, + objectId, + fieldName, + translations); + } + + @SneakyThrows + protected String saveDefaultTranslationLabel(String objectType, + long objectId, + String fieldName, + String i18nKey) { + String label = getI18NLabel(i18nKey, Locale.ENGLISH); + translationService.saveTranslationLabel(objectType, objectId, fieldName, Locale.ENGLISH, label); + return label; + } + + protected String getI18NLabel(String label, Locale locale) { + return getI18NLabel(label, locale, null); + } + + protected String getI18NLabel(String label, Locale locale, String defaultLabel) { + try { + ResourceBundle resourceBundle = getResourceBundle(locale); + if (resourceBundle != null + && resourceBundle.containsKey(label)) { + return resourceBundle.getString(label); + } + } catch (Exception e) { + LOG.debug("Resource Bundle not found with locale {}", locale, e); + } + return defaultLabel == null ? label : defaultLabel; + } + + private ResourceBundle getResourceBundle(Locale locale) { + return bundles.computeIfAbsent(locale, + l -> resourceBundleService.getResourceBundle(new String[] { "locale.portlet.Portlets", + "locale.portlet.LayoutEditor" }, + l, + PortalContainer.getInstance() + .getPortalClassLoader())); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java b/layout-service/src/main/java/io/meeds/layout/service/injection/PageTemplateImportService.java similarity index 55% rename from layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java rename to layout-service/src/main/java/io/meeds/layout/service/injection/PageTemplateImportService.java index 126ccdbc2..cc5875e71 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/injection/PageTemplateImportService.java @@ -16,7 +16,7 @@ * 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.layout.service; +package io.meeds.layout.service.injection; import java.io.File; import java.io.IOException; @@ -26,15 +26,14 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; -import java.util.Locale; import java.util.Random; -import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.exoplatform.commons.api.settings.SettingService; @@ -49,81 +48,64 @@ import org.exoplatform.portal.config.model.UnmarshalledObject; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; -import org.exoplatform.services.resources.LocaleConfigService; -import org.exoplatform.services.resources.ResourceBundleService; import org.exoplatform.services.security.ConversationState; import org.exoplatform.social.attachment.AttachmentService; import org.exoplatform.social.attachment.model.UploadedAttachmentDetail; -import org.exoplatform.social.rest.api.RestUtils; import org.exoplatform.upload.UploadResource; import io.meeds.common.ContainerTransactional; import io.meeds.layout.model.PageTemplate; import io.meeds.layout.model.PageTemplateDescriptor; import io.meeds.layout.model.PageTemplateDescriptorList; -import io.meeds.layout.plugin.PageTemplateAttachmentPlugin; -import io.meeds.layout.plugin.PageTemplateTranslationPlugin; +import io.meeds.layout.plugin.attachment.PageTemplateAttachmentPlugin; +import io.meeds.layout.plugin.translation.PageTemplateTranslationPlugin; import io.meeds.layout.rest.model.LayoutModel; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PageTemplateService; import io.meeds.layout.util.JsonUtils; -import io.meeds.social.translation.service.TranslationService; import jakarta.annotation.PostConstruct; import lombok.SneakyThrows; @Component +@Order(Ordered.LOWEST_PRECEDENCE) public class PageTemplateImportService { - private static final Scope PAGE_TEMPLATE_IMPORT_SCOPE = Scope.APPLICATION.id("PAGE_TEMPLATE_IMPORT"); + private static final Scope PAGE_TEMPLATE_IMPORT_SCOPE = Scope.APPLICATION.id("PAGE_TEMPLATE_IMPORT"); - private static final Context PAGE_TEMPLATE_CONTEXT = Context.GLOBAL.id("PAGE_TEMPLATE"); + private static final Context PAGE_TEMPLATE_CONTEXT = Context.GLOBAL.id("PAGE_TEMPLATE"); - private static final String PAGE_TEMPLATE_VERSION = "version"; + private static final String PAGE_TEMPLATE_VERSION = "version"; - private static final long PAGE_TEMPLATE_IMPORT_VERSION = 1; + private static final long PAGE_TEMPLATE_IMPORT_VERSION = 1; - private static final Log LOG = ExoLogger.getLogger(PageTemplateImportService.class); + private static final Log LOG = ExoLogger.getLogger(PageTemplateImportService.class); - private static final Random RANDOM = new Random(); + private static final Random RANDOM = new Random(); @Autowired - private LayoutAclService layoutAclService; + private LayoutTranslationImportService layoutTranslationService; @Autowired - private TranslationService translationService; + private LayoutAclService layoutAclService; @Autowired - private AttachmentService attachmentService; + private AttachmentService attachmentService; @Autowired - private LocaleConfigService localeConfigService; + private PageTemplateService pageTemplateService; @Autowired - private PageTemplateService pageTemplateService; + private SettingService settingService; @Autowired - private SettingService settingService; - - @Autowired - private ResourceBundleService resourceBundleService; - - @Autowired - private ConfigurationManager configurationManager; - - @Autowired - private PageTemplateAttachmentPlugin pageTemplateAttachmentPlugin; - - @Autowired - private PageTemplateTranslationPlugin pageTemplateTranslationPlugin; + private ConfigurationManager configurationManager; @Value("${meeds.pages.import.override:false}") - private boolean forceReimportTemplates; + private boolean forceReimportTemplates; @PostConstruct - @ContainerTransactional public void init() { - translationService.addPlugin(pageTemplateTranslationPlugin); - attachmentService.addPlugin(pageTemplateAttachmentPlugin); - CompletableFuture.runAsync(this::importPageTemplates); } @@ -135,7 +117,6 @@ && getSettingValue(PAGE_TEMPLATE_VERSION) != PAGE_TEMPLATE_IMPORT_VERSION) { forceReimportTemplates = true; } - ConversationState currentConversationState = ConversationState.getCurrent(); ConversationState.setCurrent(layoutAclService.getSuperUserConversationState()); try { Enumeration templateFiles = PortalContainer.getInstance() @@ -153,13 +134,18 @@ && getSettingValue(PAGE_TEMPLATE_VERSION) != PAGE_TEMPLATE_IMPORT_VERSION) { } }) .forEach(this::importDescriptor); + LOG.info("Importing Page Templates finished successfully"); + + LOG.info("Processing Post Page Templates import"); + layoutTranslationService.postImport(PageTemplateTranslationPlugin.OBJECT_TYPE); + LOG.info("Processing Post Page Templates import finished"); + + setSettingValue(PAGE_TEMPLATE_VERSION, PAGE_TEMPLATE_IMPORT_VERSION); } catch (Exception e) { LOG.warn("An error occurred while importing page templates", e); } finally { - ConversationState.setCurrent(currentConversationState); + ConversationState.setCurrent(null); } - setSettingValue(PAGE_TEMPLATE_VERSION, PAGE_TEMPLATE_IMPORT_VERSION); - LOG.info("Importing Page Templates finished successfully"); } protected List parseDescriptors(URL url) { @@ -179,7 +165,7 @@ protected void importDescriptor(PageTemplateDescriptor descriptor) { if (forceReimportTemplates || existingTemplateId == 0) { importPageTemplate(descriptor, existingTemplateId); } else { - LOG.info("Ignore re-importing Page Template {}", descriptorId); + LOG.debug("Ignore re-importing Page Template {}", descriptorId); } } @@ -189,11 +175,11 @@ protected void importPageTemplate(PageTemplateDescriptor d, long oldTemplateId) PageTemplate pageTemplate = createPageTemplate(d, oldTemplateId); if (forceReimportTemplates || oldTemplateId == 0 || pageTemplate.getId() != oldTemplateId) { LOG.info("Importing Page Template {} title translations", d.getId()); - saveTemplateNames(d, pageTemplate); + saveNames(d, pageTemplate); LOG.info("Importing Page Template {} description translations", d.getId()); - saveTemplateDescriptions(d, pageTemplate); + saveDescriptions(d, pageTemplate); LOG.info("Importing Page Template {} illustration", d.getId()); - saveTemplateIllustration(pageTemplate.getId(), d.getIllustrationPath()); + saveIllustration(pageTemplate.getId(), d.getIllustrationPath()); // Mark as imported setSettingValue(d.getId(), pageTemplate.getId()); } @@ -203,54 +189,18 @@ protected void importPageTemplate(PageTemplateDescriptor d, long oldTemplateId) } } - protected void saveTemplateNames(PageTemplateDescriptor d, PageTemplate pageTemplate) { - try { - translationService.deleteTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.TITLE_FIELD_NAME); - } catch (Exception e) { // NOSONAR - // Normal, when not exists - } - d.getNames() - .forEach((k, v) -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.TITLE_FIELD_NAME, - Locale.forLanguageTag(k), - v)); - String defaultName = d.getNames().get("en"); - localeConfigService.getLocalConfigs() - .stream() - .filter(config -> !StringUtils.equals(config.getLocale().toLanguageTag(), "en")) - .forEach(config -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.TITLE_FIELD_NAME, - config.getLocale(), - defaultName)); + protected void saveNames(PageTemplateDescriptor d, PageTemplate pageTemplate) { + layoutTranslationService.saveTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, + pageTemplate.getId(), + PageTemplateTranslationPlugin.TITLE_FIELD_NAME, + d.getNames()); } - protected void saveTemplateDescriptions(PageTemplateDescriptor d, PageTemplate pageTemplate) { - try { - translationService.deleteTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME); - } catch (Exception e) { // NOSONAR - // Normal, when not exists - } - d.getDescriptions() - .forEach((k, v) -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME, - Locale.forLanguageTag(k), - v)); - String defaultDescription = d.getDescriptions().get("en"); - localeConfigService.getLocalConfigs() - .stream() - .filter(config -> !StringUtils.equals(config.getLocale().toLanguageTag(), "en")) - .forEach(config -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, - pageTemplate.getId(), - PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME, - config.getLocale(), - defaultDescription)); + protected void saveDescriptions(PageTemplateDescriptor d, PageTemplate pageTemplate) { + layoutTranslationService.saveTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, + pageTemplate.getId(), + PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME, + d.getDescriptions()); } @SneakyThrows @@ -277,22 +227,7 @@ protected PageTemplate createPageTemplate(PageTemplateDescriptor d, long oldTemp } } - @SneakyThrows - protected void saveTranslationLabel(String objectType, long id, String fieldName, Locale locale, String label) { - if (PortalContainer.getInstanceIfPresent() != null) { - String i18nLabel = getI18NLabel(label, locale); - if (i18nLabel != null) { - label = i18nLabel; - } - } - translationService.saveTranslationLabel(objectType, - id, - fieldName, - locale, - label); - } - - protected void saveTemplateIllustration(long pageTemplateId, String imagePath) { + protected void saveIllustration(long pageTemplateId, String imagePath) { try { URL resource = configurationManager.getResource(imagePath); String uploadId = "PageTemplateIllustration" + RANDOM.nextLong(); @@ -306,7 +241,7 @@ protected void saveTemplateIllustration(long pageTemplateId, String imagePath) { PageTemplateAttachmentPlugin.OBJECT_TYPE, String.valueOf(pageTemplateId), null, - RestUtils.getCurrentUserIdentityId()); + layoutAclService.getSuperUserIdentityId()); } catch (Exception e) { throw new IllegalStateException(String.format("Error while saving Image '%s' as attachment for template '%s'", imagePath, @@ -321,22 +256,6 @@ protected Container fromXML(String xml) { return obj.getObject(); } - protected String getI18NLabel(String label, Locale locale) { - try { - ResourceBundle resourceBundle = - resourceBundleService.getResourceBundle("locale.portlet.Portlets", - locale, - PortalContainer.getInstance() - .getPortalClassLoader()); - if (resourceBundle != null && resourceBundle.containsKey(label)) { - return resourceBundle.getString(label); - } - } catch (Exception e) { - LOG.debug("Resource Bundle not found with locale {}", locale, e); - } - return null; - } - protected void setSettingValue(String name, long value) { settingService.set(PAGE_TEMPLATE_CONTEXT, PAGE_TEMPLATE_IMPORT_SCOPE, diff --git a/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java new file mode 100644 index 000000000..ee41a99e0 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java @@ -0,0 +1,369 @@ +/** + * 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.layout.service.injection; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.api.settings.SettingService; +import org.exoplatform.commons.api.settings.SettingValue; +import org.exoplatform.commons.api.settings.data.Context; +import org.exoplatform.commons.api.settings.data.Scope; +import org.exoplatform.container.configuration.ConfigurationManager; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.services.security.ConversationState; +import org.exoplatform.social.attachment.AttachmentService; +import org.exoplatform.social.attachment.model.UploadedAttachmentDetail; +import org.exoplatform.upload.UploadResource; + +import io.meeds.common.ContainerTransactional; +import io.meeds.layout.model.PortletDescriptor; +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.model.PortletInstanceCategoryDescriptor; +import io.meeds.layout.model.PortletInstanceCategoryDescriptorList; +import io.meeds.layout.model.PortletInstanceDescriptor; +import io.meeds.layout.model.PortletInstanceDescriptorList; +import io.meeds.layout.plugin.attachment.PortletInstanceAttachmentPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceCategoryTranslationPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceTranslationPlugin; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.layout.service.PortletService; +import io.meeds.layout.util.JsonUtils; + +import jakarta.annotation.PostConstruct; +import lombok.SneakyThrows; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE) +public class PortletInstanceImportService { + + private static final String PORTLET_INSTANCE_IMPORT = "PORTLET_INSTANCE_IMPORT"; + + private static final Scope PORTLET_INSTANCE_IMPORT_SCOPE = + Scope.APPLICATION.id(PORTLET_INSTANCE_IMPORT); + + private static final Scope PORTLET_INSTANCE_CATEGORY_IMPORT_SCOPE = + Scope.APPLICATION.id("PORTLET_INSTANCE_CATEGORY_IMPORT"); + + private static final Context PORTLET_INSTANCE_CONTEXT = Context.GLOBAL.id("PORTLET_INSTANCE"); + + private static final String PORTLET_INSTANCE_VERSION = "version"; + + private static final long PORTLET_INSTANCE_IMPORT_VERSION = 1; + + private static final Log LOG = + ExoLogger.getLogger(PortletInstanceImportService.class); + + private static final Random RANDOM = new Random(); + + @Autowired + private LayoutAclService layoutAclService; + + @Autowired + private LayoutTranslationImportService layoutTranslationService; + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletService portletService; + + @Autowired + private SettingService settingService; + + @Autowired + private ConfigurationManager configurationManager; + + @Value("${meeds.portlets.import.override:false}") + private boolean forceReimport; + + @PostConstruct + public void init() { + CompletableFuture.runAsync(this::importPortletInstances); + } + + @ContainerTransactional + public void importPortletInstances() { + LOG.info("Importing Portlet instances"); + if (!forceReimport + && getSettingValue(PORTLET_INSTANCE_VERSION) != PORTLET_INSTANCE_IMPORT_VERSION) { + forceReimport = true; + } + + ConversationState.setCurrent(layoutAclService.getSuperUserConversationState()); + try { + Collections.list(getClassLoader().getResources("portlet-instance-categories.json")) + .stream() + .map(this::parseCategoryDescriptors) + .flatMap(List::stream) + .forEach(this::importCategoryDescriptor); + Collections.list(getClass().getClassLoader().getResources("portlet-instances.json")) + .stream() + .map(this::parseDescriptors) + .flatMap(List::stream) + .forEach(this::importDescriptor); + LOG.info("Importing Portlet instances finished successfully."); + + LOG.info("Processing Post Portlet instances import"); + layoutTranslationService.postImport(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE); + layoutTranslationService.postImport(PortletInstanceTranslationPlugin.OBJECT_TYPE); + LOG.info("Processing Post Portlet instances import finished"); + + setSettingValue(PORTLET_INSTANCE_VERSION, PORTLET_INSTANCE_IMPORT_VERSION); + } catch (Exception e) { + LOG.warn("An error occurred while importing portlet instances", e); + } finally { + ConversationState.setCurrent(null); + } + } + + protected List parseDescriptors(URL url) { + try (InputStream inputStream = url.openStream()) { + String content = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + PortletInstanceDescriptorList list = JsonUtils.fromJsonString(content, PortletInstanceDescriptorList.class); + return list.getDescriptors(); + } catch (IOException e) { + LOG.warn("An unkown error happened while parsing portlet instances from url {}", url, e); + return Collections.emptyList(); + } + } + + protected List parseCategoryDescriptors(URL url) { + try (InputStream inputStream = url.openStream()) { + String content = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + PortletInstanceCategoryDescriptorList list = JsonUtils.fromJsonString(content, PortletInstanceCategoryDescriptorList.class); + return list.getDescriptors(); + } catch (IOException e) { + LOG.warn("An unkown error happened while parsing portlet instances from url {}", url, e); + return Collections.emptyList(); + } + } + + protected void importCategoryDescriptor(PortletInstanceCategoryDescriptor descriptor) { + String descriptorId = descriptor.getNameId(); + long existingId = getCategorySettingValue(descriptorId); + if (forceReimport || existingId == 0) { + importPortletInstanceCategory(descriptor, existingId); + } else { + LOG.debug("Ignore re-importing Portlet instance category {}", descriptorId); + } + } + + protected void importDescriptor(PortletInstanceDescriptor descriptor) { + String descriptorId = descriptor.getNameId(); + long existingId = getSettingValue(descriptorId); + if (forceReimport || existingId == 0) { + importPortletInstance(descriptor, existingId); + } else { + LOG.debug("Ignore re-importing Portlet instance {}", descriptorId); + } + } + + protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d, long oldId) { + String descriptorId = d.getNameId(); + LOG.info("Importing Portlet category instance {}", descriptorId); + try { + PortletInstanceCategory category = savePortletInstanceCategory(d, oldId); + if (forceReimport || oldId == 0 || category.getId() != oldId) { + LOG.info("Importing Portlet instance category {} title translations", descriptorId); + saveCategoryNames(d, category); + LOG.info("Importing Portlet instance category {} description translations", descriptorId); + saveCategoryDescriptions(d, category); + // Mark as imported + setCategorySettingValue(descriptorId, category.getId()); + } + LOG.info("Importing Portlet instance category {} finished successfully", descriptorId); + } catch (Exception e) { + LOG.warn("An error occurred while importing portlet instance category {}", descriptorId, e); + } + } + + protected void importPortletInstance(PortletInstanceDescriptor d, long oldId) { + String descriptorId = d.getNameId(); + LOG.info("Importing Portlet instance {}", descriptorId); + try { + PortletInstance portletInstance = savePortletInstance(d, oldId); + if (forceReimport || oldId == 0 || portletInstance.getId() != oldId) { + LOG.info("Importing Portlet instance {} title translations", descriptorId); + saveNames(d, portletInstance); + LOG.info("Importing Portlet instance {} description translations", descriptorId); + saveDescriptions(d, portletInstance); + LOG.info("Importing Portlet instance {} illustration", descriptorId); + saveIllustration(portletInstance.getId(), d.getIllustrationPath()); + // Mark as imported + setSettingValue(descriptorId, portletInstance.getId()); + } + LOG.debug("Importing Portlet instance {} finished successfully", descriptorId); + } catch (Exception e) { + LOG.warn("An error occurred while importing portlet instance {}", descriptorId, e); + } + } + + protected void saveNames(PortletInstanceDescriptor d, PortletInstance portletInstance) { + layoutTranslationService.saveTranslationLabels(PortletInstanceTranslationPlugin.OBJECT_TYPE, + portletInstance.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME, + d.getNames()); + } + + protected void saveDescriptions(PortletInstanceDescriptor d, PortletInstance portletInstance) { + layoutTranslationService.saveTranslationLabels(PortletInstanceTranslationPlugin.OBJECT_TYPE, + portletInstance.getId(), + PortletInstanceTranslationPlugin.DESCRIPTION_FIELD_NAME, + d.getDescriptions()); + } + + protected void saveCategoryNames(PortletInstanceCategoryDescriptor d, PortletInstanceCategory category) { + layoutTranslationService.saveTranslationLabels(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME, + d.getNames()); + } + + protected void saveCategoryDescriptions(PortletInstanceCategoryDescriptor d, PortletInstanceCategory category) { + layoutTranslationService.saveTranslationLabels(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.DESCRIPTION_FIELD_NAME, + d.getDescriptions()); + } + + @SneakyThrows + protected PortletInstanceCategory savePortletInstanceCategory(PortletInstanceCategoryDescriptor d, long oldId) { + PortletInstanceCategory category = null; + if (oldId > 0) { + category = portletInstanceService.getPortletInstanceCategory(oldId); + } + boolean isNew = category == null; + if (isNew) { + category = new PortletInstanceCategory(); + } + category.setIcon(d.getIcon()); + category.setPermissions(d.getPermissions()); + category.setSystem(d.isSystem()); + if (isNew) { + return portletInstanceService.createPortletInstanceCategory(category); + } else { + return portletInstanceService.updatePortletInstanceCategory(category); + } + } + + @SneakyThrows + protected PortletInstance savePortletInstance(PortletInstanceDescriptor d, long oldId) { + PortletDescriptor portlet = portletService.getPortlet(d.getPortletName()); + PortletInstance portletInstance = null; + if (oldId > 0) { + portletInstance = portletInstanceService.getPortletInstance(oldId); + } + boolean isNew = portletInstance == null; + if (isNew) { + portletInstance = new PortletInstance(); + } + portletInstance.setContentId(portlet.getContentId()); + portletInstance.setCategoryId(getCategorySettingValue(d.getCategoryNameId())); + portletInstance.setPermissions(d.getPermissions()); + portletInstance.setPreferences(d.getPreferences()); + portletInstance.setSpaceApplication(d.isSpaceApplication()); + portletInstance.setSystem(d.isSystem()); + if (isNew) { + return portletInstanceService.createPortletInstance(portletInstance); + } else { + return portletInstanceService.updatePortletInstance(portletInstance); + } + } + + protected void saveIllustration(long portletInstanceId, String imagePath) { + try { + URL resource = configurationManager.getResource(imagePath); + String uploadId = "PortletInstanceIllustration" + RANDOM.nextLong(); + UploadResource uploadResource = new UploadResource(uploadId); + uploadResource.setFileName(new File(resource.getPath()).getName()); + uploadResource.setMimeType("image/png"); + uploadResource.setStatus(UploadResource.UPLOADED_STATUS); + uploadResource.setStoreLocation(resource.getPath()); + UploadedAttachmentDetail uploadedAttachmentDetail = new UploadedAttachmentDetail(uploadResource); + attachmentService.saveAttachment(uploadedAttachmentDetail, + PortletInstanceAttachmentPlugin.OBJECT_TYPE, + String.valueOf(portletInstanceId), + null, + layoutAclService.getSuperUserIdentityId()); + } catch (Exception e) { + throw new IllegalStateException(String.format("Error while saving Image '%s' as attachment for portlet instance '%s'", + imagePath, + portletInstanceId), + e); + } + } + + protected void setCategorySettingValue(String name, long value) { + settingService.set(PORTLET_INSTANCE_CONTEXT, + PORTLET_INSTANCE_CATEGORY_IMPORT_SCOPE, + name, + SettingValue.create(String.valueOf(value))); + } + + protected long getCategorySettingValue(String name) { + try { + SettingValue settingValue = settingService.get(PORTLET_INSTANCE_CONTEXT, PORTLET_INSTANCE_CATEGORY_IMPORT_SCOPE, name); + return settingValue == null || settingValue.getValue() == null ? 0l : Long.parseLong(settingValue.getValue().toString()); + } catch (NumberFormatException e) { + return 0l; + } + } + + protected void setSettingValue(String name, long value) { + settingService.set(PORTLET_INSTANCE_CONTEXT, + PORTLET_INSTANCE_IMPORT_SCOPE, + name, + SettingValue.create(String.valueOf(value))); + } + + protected long getSettingValue(String name) { + try { + SettingValue settingValue = settingService.get(PORTLET_INSTANCE_CONTEXT, PORTLET_INSTANCE_IMPORT_SCOPE, name); + return settingValue == null || settingValue.getValue() == null ? 0l : Long.parseLong(settingValue.getValue().toString()); + } catch (NumberFormatException e) { + return 0l; + } + } + + private ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceCategoryStorage.java b/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceCategoryStorage.java new file mode 100644 index 000000000..fe3defa90 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceCategoryStorage.java @@ -0,0 +1,78 @@ +/** + * 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.layout.storage; + +import static io.meeds.layout.util.EntityMapper.*; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.dao.PortletInstanceCategoryDAO; +import io.meeds.layout.entity.PortletInstanceCategoryEntity; +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.util.EntityMapper; + +@Component +public class PortletInstanceCategoryStorage { + + @Autowired + private PortletInstanceCategoryDAO portletInstanceCategoryDAO; + + public List getPortletInstanceCategories() { + List entities = portletInstanceCategoryDAO.findAll(); + return entities.stream() + .map(EntityMapper::fromEntity) + .toList(); + } + + public PortletInstanceCategory getPortletInstanceCategory(long id) { + return portletInstanceCategoryDAO.findById(id) + .map(EntityMapper::fromEntity) + .orElse(null); + } + + public PortletInstanceCategory createPortletInstanceCategory(PortletInstanceCategory portletInstanceCategory) { + return save(portletInstanceCategory); + } + + public PortletInstanceCategory updatePortletInstanceCategory(PortletInstanceCategory portletInstanceCategory) throws ObjectNotFoundException { + if (!portletInstanceCategoryDAO.existsById(portletInstanceCategory.getId())) { + throw new ObjectNotFoundException("Entity doesn't exist"); + } + return save(portletInstanceCategory); + } + + public void deletePortletInstanceCategory(long id) throws ObjectNotFoundException { + if (!portletInstanceCategoryDAO.existsById(id)) { + throw new ObjectNotFoundException(String.format("Entity with id %s doesn't exist", id)); + } + portletInstanceCategoryDAO.deleteById(id); + } + + private PortletInstanceCategory save(PortletInstanceCategory portletInstanceCategory) { + PortletInstanceCategoryEntity entity = toEntity(portletInstanceCategory); + entity = portletInstanceCategoryDAO.save(entity); + return fromEntity(entity); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceStorage.java b/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceStorage.java new file mode 100644 index 000000000..9b902bbaf --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/storage/PortletInstanceStorage.java @@ -0,0 +1,92 @@ +/** + * 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.layout.storage; + +import static io.meeds.layout.util.EntityMapper.fromEntity; +import static io.meeds.layout.util.EntityMapper.toEntity; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.dao.PortletInstanceDAO; +import io.meeds.layout.entity.PortletInstanceEntity; +import io.meeds.layout.model.PortletInstance; + +@Component +public class PortletInstanceStorage { + + @Autowired + private PortletStorage portletStorage; + + @Autowired + private PortletInstanceDAO portletInstanceDAO; + + public List getPortletInstances() { + List entities = portletInstanceDAO.findAll(); + return entities.stream() + .map(entity -> fromEntity(entity, + portletStorage.getPortletDescriptor(entity.getContentId()))) + .toList(); + } + + public List getPortletInstances(long categoryId) { + List entities = portletInstanceDAO.findByCategoryId(categoryId); + return entities.stream() + .map(entity -> fromEntity(entity, + portletStorage.getPortletDescriptor(entity.getContentId()))) + .toList(); + } + + public PortletInstance getPortletInstance(long id) { + return portletInstanceDAO.findById(id) + .map(entity -> fromEntity(entity, + portletStorage.getPortletDescriptor(entity.getContentId()))) + .orElse(null); + } + + public PortletInstance createPortletInstance(PortletInstance portletInstance) { + return save(portletInstance); + } + + public PortletInstance updatePortletInstance(PortletInstance portletInstance) throws ObjectNotFoundException { + if (!portletInstanceDAO.existsById(portletInstance.getId())) { + throw new ObjectNotFoundException("Entity doesn't exist"); + } + return save(portletInstance); + } + + public void deletePortletInstance(long id) throws ObjectNotFoundException { + if (!portletInstanceDAO.existsById(id)) { + throw new ObjectNotFoundException(String.format("Entity with id %s doesn't exist", id)); + } + portletInstanceDAO.deleteById(id); + } + + private PortletInstance save(PortletInstance portletInstance) { + PortletInstanceEntity entity = toEntity(portletInstance); + entity = portletInstanceDAO.save(entity); + return fromEntity(entity, + portletStorage.getPortletDescriptor(entity.getContentId())); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/storage/PortletStorage.java b/layout-service/src/main/java/io/meeds/layout/storage/PortletStorage.java new file mode 100644 index 000000000..597252122 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/storage/PortletStorage.java @@ -0,0 +1,114 @@ +/** + * 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.layout.storage; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.gatein.common.i18n.LocalizedString; +import org.gatein.pc.api.Portlet; +import org.gatein.pc.api.PortletInvoker; +import org.gatein.pc.api.info.MetaInfo; +import org.gatein.pc.api.info.ModeInfo; +import org.gatein.pc.api.info.PortletInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.container.ExoContainerContext; + +import io.meeds.layout.model.PortletDescriptor; + +import lombok.Setter; +import lombok.SneakyThrows; + +@Component +public class PortletStorage { + + // Can't be injected using @Autowired since it's injected by code + // in org.exoplatform.portal.pc.ExoKernelIntegration + @Autowired(required = false) + private PortletInvoker portletInvoker; + + @Setter + private List portletDescriptors; + + public List getPortletDescriptors() { + if (portletDescriptors == null) { + portletDescriptors = Collections.unmodifiableList(getPortlets().stream() + .map(this::toPortletDescriptor) + .filter(Objects::nonNull) + .toList()); + } + return portletDescriptors; + } + + public PortletDescriptor getPortletDescriptor(String id) { + return getPortletDescriptors().stream() + .filter(p -> StringUtils.equals(p.getContentId(), id) + || StringUtils.equals(p.getPortletName(), id)) + .findFirst() + .orElse(null); + } + + private PortletDescriptor toPortletDescriptor(Portlet portlet) { + if (portlet.isRemote()) { + return null; + } + PortletInfo info = portlet.getInfo(); + MetaInfo metaInfo = info.getMeta(); + Set allModes = info.getCapabilities() + .getModes(org.gatein.common.net.media.MediaType.create("text/html")); + LocalizedString descriptionLS = metaInfo.getMetaValue(MetaInfo.DESCRIPTION); + LocalizedString nameLS = metaInfo.getMetaValue(MetaInfo.DISPLAY_NAME); + + PortletDescriptor portletDescriptor = new PortletDescriptor(); + portletDescriptor.setApplicationName(info.getApplicationName()); + portletDescriptor.setPortletName(info.getName()); + portletDescriptor.setName(getLocalizedStringOrDefault(nameLS, info.getName())); + portletDescriptor.setDescription(getLocalizedStringOrDefault(descriptionLS, info.getName())); + portletDescriptor.setSupportedModes(allModes.stream() + .map(m -> m.getModeName()) + .toList()); + return portletDescriptor; + } + + private String getLocalizedStringOrDefault(LocalizedString localizedString, String portletName) { + if (localizedString == null || localizedString.getDefaultString() == null) { + return portletName; + } else { + return localizedString.getDefaultString(); + } + } + + @SneakyThrows + private Set getPortlets() { + return getPortletInvoker().getPortlets(); + } + + private PortletInvoker getPortletInvoker() { + if (portletInvoker == null) { + portletInvoker = ExoContainerContext.getService(PortletInvoker.class); + } + return portletInvoker; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java new file mode 100644 index 000000000..6868a4422 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java @@ -0,0 +1,110 @@ +/** + * 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.layout.util; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import io.meeds.layout.entity.PortletInstanceCategoryEntity; +import io.meeds.layout.entity.PortletInstanceEntity; +import io.meeds.layout.model.PortletDescriptor; +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.model.PortletInstancePreference; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class EntityMapper { + private EntityMapper() { + // Utils Class + } + + public static PortletInstanceCategory fromEntity(PortletInstanceCategoryEntity entity) { + return new PortletInstanceCategory(entity.getId(), + null, + null, + entity.getIcon(), + entity.isSystem(), + entity.getPermissions()); + } + + public static PortletInstance fromEntity(PortletInstanceEntity entity, PortletDescriptor portlet) { + return new PortletInstance(entity.getId(), + null, + null, + entity.getCategoryId(), + entity.getContentId(), + getPreferences(entity), + 0l, + entity.getPermissions(), + portlet == null ? null : portlet.getSupportedModes(), + entity.isSystem(), + entity.isDisabled(), + entity.isSpaceApplication()); + } + + public static PortletInstanceCategoryEntity toEntity(PortletInstanceCategory instance) { + return new PortletInstanceCategoryEntity(instance.getId() == 0 ? null : instance.getId(), + instance.getPermissions(), + instance.getIcon(), + instance.isSystem()); + } + + public static PortletInstanceEntity toEntity(PortletInstance instance) { + return new PortletInstanceEntity(instance.getId() == 0 ? null : instance.getId(), + instance.getCategoryId(), + instance.getContentId(), + instance.getPermissions(), + getPreferencesString(instance), + instance.isSystem(), + instance.isSpaceApplication(), + instance.isDisabled(), + Objects.hash(instance.getCategoryId(), + getPreferencesString(instance), + instance.getContentId())); + } + + private static String getPreferencesString(PortletInstance instance) { + if (CollectionUtils.isNotEmpty(instance.getPreferences())) { + return JsonUtils.toJsonString(new PortletInstancePreferences(instance.getPreferences())); + } + return null; + } + + private static List getPreferences(PortletInstanceEntity entity) { + if (StringUtils.isNotBlank(entity.getPreferences())) { + return JsonUtils.fromJsonString(entity.getPreferences(), PortletInstancePreferences.class).getPreferences(); + } + return Collections.emptyList(); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class PortletInstancePreferences { + private List preferences; + } + +} diff --git a/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml b/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml index 771a4c1e7..d9a757690 100644 --- a/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml +++ b/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml @@ -22,7 +22,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.25.xsd"> @@ -90,4 +90,480 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE PORTAL_APPLICATIONS MODIFY COLUMN PREFERENCES LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + + + + + + + + + + + \ No newline at end of file diff --git a/layout-service/src/main/resources/jpa-entities.idx b/layout-service/src/main/resources/jpa-entities.idx index eaa478f0c..975ff0707 100644 --- a/layout-service/src/main/resources/jpa-entities.idx +++ b/layout-service/src/main/resources/jpa-entities.idx @@ -1 +1,3 @@ io.meeds.layout.entity.PageTemplateEntity +io.meeds.layout.entity.PortletInstanceEntity +io.meeds.layout.entity.PortletInstanceCategoryEntity diff --git a/layout-service/src/main/resources/portlet-instance-categories.json b/layout-service/src/main/resources/portlet-instance-categories.json new file mode 100644 index 000000000..cca24904d --- /dev/null +++ b/layout-service/src/main/resources/portlet-instance-categories.json @@ -0,0 +1,60 @@ +{ + "descriptors":[ + { + "nameId":"spaces", + "names":{ + "en":"layout.portletInstance.category.spaces.name" + }, + "descriptions":{ + "en":"layout.portletInstance.category.spaces.description" + }, + "icon":"fa-braille", + "permissions":[ + "*:/platform/users" + ], + "system":true + }, + { + "nameId":"content", + "names":{ + "en":"layout.portletInstance.category.content.name" + }, + "descriptions":{ + "en":"layout.portletInstance.category.content.description" + }, + "icon":"fa-newspaper", + "permissions":[ + "*:/platform/users" + ], + "system":true + }, + { + "nameId":"tools", + "names":{ + "en":"layout.portletInstance.category.tools.name" + }, + "descriptions":{ + "en":"layout.portletInstance.category.tools.description" + }, + "icon":"fa-tools", + "permissions":[ + "*:/platform/users" + ], + "system":true + }, + { + "nameId":"navigation", + "names":{ + "en":"layout.portletInstance.category.navigation.name" + }, + "descriptions":{ + "en":"layout.portletInstance.category.navigation.description" + }, + "icon":"fa-columns", + "permissions":[ + "*:/platform/users" + ], + "system":true + } + ] +} \ No newline at end of file diff --git a/layout-service/src/main/resources/portlet-instances.json b/layout-service/src/main/resources/portlet-instances.json new file mode 100644 index 000000000..543e357a6 --- /dev/null +++ b/layout-service/src/main/resources/portlet-instances.json @@ -0,0 +1,304 @@ +{ + "descriptors":[ + { + "nameId":"MembersPortlet", + "categoryNameId":"spaces", + "portletName":"MembersPortlet", + "names":{ + "en":"layout.portletInstance.MembersPortlet.name" + }, + "descriptions":{ + "en":"layout.portletInstance.MembersPortlet.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/MembersPortlet.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":true + }, + { + "nameId":"SpaceSettingPortlet", + "categoryNameId":"spaces", + "portletName":"SpaceSettingPortlet", + "names":{ + "en":"layout.portletInstance.SpaceSettingPortlet.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SpaceSettingPortlet.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SpaceSettingPortlet.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":true + }, + { + "nameId":"Image", + "categoryNameId":"content", + "portletName":"Image", + "names":{ + "en":"layout.portletInstance.Image.name" + }, + "descriptions":{ + "en":"layout.portletInstance.Image.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/Image.png", + "preferences":[ + + ], + "permissions":[ + "Everyone" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"Links", + "categoryNameId":"content", + "portletName":"Links", + "names":{ + "en":"layout.portletInstance.Links.name" + }, + "descriptions":{ + "en":"layout.portletInstance.Links.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/Links.png", + "preferences":[ + + ], + "permissions":[ + "Everyone" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"ExternalSpacesList", + "categoryNameId":"tools", + "portletName":"ExternalSpacesList", + "names":{ + "en":"layout.portletInstance.ExternalSpacesList.name" + }, + "descriptions":{ + "en":"layout.portletInstance.ExternalSpacesList.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/ExternalSpacesList.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"SpaceBannerPortlet", + "categoryNameId":"tools", + "portletName":"SpaceBannerPortlet", + "names":{ + "en":"layout.portletInstance.SpaceBannerPortlet.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SpaceBannerPortlet.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SpaceMenuPortlet.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"WhoIsOnLinePortlet", + "categoryNameId":"tools", + "portletName":"WhoIsOnLinePortlet", + "names":{ + "en":"layout.portletInstance.WhoIsOnLinePortlet.name" + }, + "descriptions":{ + "en":"layout.portletInstance.WhoIsOnLinePortlet.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/WhoIsOnLinePortlet.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"PeopleOverview", + "categoryNameId":"tools", + "portletName":"PeopleOverview", + "names":{ + "en":"layout.portletInstance.PeopleOverview.name" + }, + "descriptions":{ + "en":"layout.portletInstance.PeopleOverview.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/PeopleOverview.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"SpaceActivityStreamPortlet", + "categoryNameId":"tools", + "portletName":"SpaceActivityStreamPortlet", + "names":{ + "en":"layout.portletInstance.SpaceActivityStreamPortlet.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SpaceActivityStreamPortlet.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SpaceActivityStreamPortlet.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"SpacesOverview", + "categoryNameId":"tools", + "portletName":"SpacesOverview", + "names":{ + "en":"layout.portletInstance.SpacesOverview.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SpacesOverview.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SpacesOverview.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"SuggestionsPeopleAndSpace", + "categoryNameId":"tools", + "portletName":"SuggestionsPeopleAndSpace", + "names":{ + "en":"layout.portletInstance.SuggestionsPeopleAndSpace.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SuggestionsPeopleAndSpace.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SuggestionsPeopleAndSpace.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"SpaceInfos", + "categoryNameId":"tools", + "portletName":"SpaceInfos", + "names":{ + "en":"layout.portletInstance.SpaceInfos.name" + }, + "descriptions":{ + "en":"layout.portletInstance.SpaceInfos.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/SpaceInfos.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"VerticalMenu", + "categoryNameId":"navigation", + "portletName":"VerticalMenu", + "names":{ + "en":"layout.portletInstance.VerticalMenu.name" + }, + "descriptions":{ + "en":"layout.portletInstance.VerticalMenu.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/VerticalMenu.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"VerticalMenu", + "categoryNameId":"navigation", + "portletName":"VerticalMenu", + "names":{ + "en":"layout.portletInstance.VerticalMenu.name" + }, + "descriptions":{ + "en":"layout.portletInstance.VerticalMenu.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/VerticalMenu.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + }, + { + "nameId":"Breadcrumb", + "categoryNameId":"navigation", + "portletName":"Breadcrumb", + "names":{ + "en":"layout.portletInstance.Breadcrumb.name" + }, + "descriptions":{ + "en":"layout.portletInstance.Breadcrumb.description" + }, + "illustrationPath":"war:/../skin/DefaultSkin/portletIcons/Breadcrumb.png", + "preferences":[ + + ], + "permissions":[ + "*:/platform/users" + ], + "system":true, + "spaceApplication":false + } + ] +} \ No newline at end of file diff --git a/layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateAttachmentPluginTest.java b/layout-service/src/test/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPluginTest.java similarity index 89% rename from layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateAttachmentPluginTest.java rename to layout-service/src/test/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPluginTest.java index 6e4bc2e30..66983c582 100644 --- a/layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateAttachmentPluginTest.java +++ b/layout-service/src/test/java/io/meeds/layout/plugin/attachment/PageTemplateAttachmentPluginTest.java @@ -16,11 +16,11 @@ * 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.layout.plugin; +package io.meeds.layout.plugin.attachment; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; @@ -32,13 +32,14 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentService; import io.meeds.layout.service.LayoutAclService; import lombok.SneakyThrows; @SpringBootTest(classes = { - PageTemplateAttachmentPlugin.class, + PageTemplateAttachmentPlugin.class, }) @ExtendWith(MockitoExtension.class) public class PageTemplateAttachmentPluginTest { @@ -46,6 +47,9 @@ public class PageTemplateAttachmentPluginTest { @Mock private Identity userIdentity; + @MockBean + private AttachmentService attachmentService; + @MockBean private LayoutAclService layoutAclService; diff --git a/layout-service/src/test/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPluginTest.java b/layout-service/src/test/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPluginTest.java new file mode 100644 index 000000000..e0bbbb417 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/plugin/attachment/PortletInstanceAttachmentPluginTest.java @@ -0,0 +1,117 @@ +/** + * 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.layout.plugin.attachment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { + PortletInstanceAttachmentPlugin.class, +}) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceAttachmentPluginTest { + + @Mock + private Identity userIdentity; + + @MockBean + private AttachmentService attachmentService; + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletInstanceAttachmentPlugin attachmentPlugin; + + @Test + public void getObjectType() { + assertEquals("portletInstance", attachmentPlugin.getObjectType()); + assertEquals(PortletInstanceAttachmentPlugin.OBJECT_TYPE, attachmentPlugin.getObjectType()); + } + + @Test + @SneakyThrows + public void hasEditPermission() { + assertFalse(attachmentPlugin.hasEditPermission(null, null)); + assertFalse(attachmentPlugin.hasEditPermission(userIdentity, null)); + when(userIdentity.getUserId()).thenReturn("test"); + when(layoutAclService.isAdministrator(userIdentity.getUserId())).thenReturn(true); + assertTrue(attachmentPlugin.hasEditPermission(userIdentity, null)); + } + + @Test + @SneakyThrows + public void hasAccessPermission() { + assertThrows(ObjectNotFoundException.class, () -> attachmentPlugin.hasAccessPermission(null, "1")); + PortletInstance portletInstance = mock(PortletInstance.class); + when(portletInstanceService.getPortletInstance(1)).thenReturn(portletInstance); + assertTrue(attachmentPlugin.hasAccessPermission(null, "1")); + + String username = "user"; + when(userIdentity.getUserId()).thenReturn(username); + assertTrue(attachmentPlugin.hasAccessPermission(userIdentity, "1")); + + String permissionExpression = "A Permission Expression"; + when(portletInstance.getPermissions()).thenReturn(Collections.singletonList(permissionExpression)); + assertFalse(attachmentPlugin.hasAccessPermission(userIdentity, "1")); + + when(layoutAclService.isMemberOf(username, permissionExpression)).thenReturn(true); + assertTrue(attachmentPlugin.hasAccessPermission(userIdentity, "1")); + } + + @Test + @SneakyThrows + public void getAudienceId() { + assertEquals(0l, attachmentPlugin.getAudienceId(null)); + } + + @Test + @SneakyThrows + public void getSpaceId() { + assertEquals(0l, attachmentPlugin.getSpaceId("")); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateTranslationPluginTest.java b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPluginTest.java similarity index 86% rename from layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateTranslationPluginTest.java rename to layout-service/src/test/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPluginTest.java index f9988738e..e4e58d571 100644 --- a/layout-service/src/test/java/io/meeds/layout/plugin/PageTemplateTranslationPluginTest.java +++ b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PageTemplateTranslationPluginTest.java @@ -16,11 +16,11 @@ * 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.layout.plugin; +package io.meeds.layout.plugin.translation; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; @@ -31,11 +31,12 @@ import org.springframework.boot.test.mock.mockito.MockBean; import io.meeds.layout.service.LayoutAclService; +import io.meeds.social.translation.service.TranslationService; import lombok.SneakyThrows; @SpringBootTest(classes = { - PageTemplateTranslationPlugin.class, + PageTemplateTranslationPlugin.class, }) @ExtendWith(MockitoExtension.class) public class PageTemplateTranslationPluginTest { @@ -43,10 +44,13 @@ public class PageTemplateTranslationPluginTest { @MockBean private LayoutAclService layoutAclService; + @MockBean + private TranslationService translationService; + @Autowired private PageTemplateTranslationPlugin translationPlugin; - String username = "test"; + private String username = "test"; @Test public void getObjectType() { diff --git a/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPluginTest.java b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPluginTest.java new file mode 100644 index 000000000..9eab3a18a --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceCategoryTranslationPluginTest.java @@ -0,0 +1,110 @@ +/** + * 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.layout.plugin.translation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.social.translation.service.TranslationService; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { + PortletInstanceCategoryTranslationPlugin.class, +}) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceCategoryTranslationPluginTest { + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private TranslationService translationService; + + @MockBean + private PortletInstanceService portletInstanceCategoryService; + + @Autowired + private PortletInstanceCategoryTranslationPlugin translationPlugin; + + private String username = "test"; + + @Test + public void getObjectType() { + assertEquals("portletInstanceCategory", translationPlugin.getObjectType()); + assertEquals(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, translationPlugin.getObjectType()); + } + + @Test + @SneakyThrows + public void hasEditPermission() { + assertFalse(translationPlugin.hasEditPermission(0l, null)); + assertFalse(translationPlugin.hasEditPermission(0l, username)); + when(layoutAclService.isAdministrator(username)).thenReturn(true); + assertTrue(translationPlugin.hasEditPermission(0l, username)); + } + + @Test + @SneakyThrows + public void hasAccessPermission() { + assertThrows(ObjectNotFoundException.class, () -> translationPlugin.hasAccessPermission(1, null)); + PortletInstanceCategory portletInstanceCategory = mock(PortletInstanceCategory.class); + when(portletInstanceCategoryService.getPortletInstanceCategory(1)).thenReturn(portletInstanceCategory); + assertTrue(translationPlugin.hasAccessPermission(1, null)); + assertTrue(translationPlugin.hasAccessPermission(1, username)); + + String permissionExpression = "A Permission Expression"; + when(portletInstanceCategory.getPermissions()).thenReturn(Collections.singletonList(permissionExpression)); + assertFalse(translationPlugin.hasAccessPermission(1, username)); + + when(layoutAclService.isMemberOf(username, permissionExpression)).thenReturn(true); + assertTrue(translationPlugin.hasAccessPermission(1, username)); + } + + @Test + @SneakyThrows + public void getAudienceId() { + assertEquals(0l, translationPlugin.getAudienceId(0l)); + } + + @Test + @SneakyThrows + public void getSpaceId() { + assertEquals(0l, translationPlugin.getSpaceId(0l)); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPluginTest.java b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPluginTest.java new file mode 100644 index 000000000..b387a83f7 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/plugin/translation/PortletInstanceTranslationPluginTest.java @@ -0,0 +1,110 @@ +/** + * 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.layout.plugin.translation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.social.translation.service.TranslationService; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { + PortletInstanceTranslationPlugin.class, +}) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceTranslationPluginTest { + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private TranslationService translationService; + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletInstanceTranslationPlugin translationPlugin; + + private String username = "test"; + + @Test + public void getObjectType() { + assertEquals("portletInstance", translationPlugin.getObjectType()); + assertEquals(PortletInstanceTranslationPlugin.OBJECT_TYPE, translationPlugin.getObjectType()); + } + + @Test + @SneakyThrows + public void hasEditPermission() { + assertFalse(translationPlugin.hasEditPermission(0l, null)); + assertFalse(translationPlugin.hasEditPermission(0l, username)); + when(layoutAclService.isAdministrator(username)).thenReturn(true); + assertTrue(translationPlugin.hasEditPermission(0l, username)); + } + + @Test + @SneakyThrows + public void hasAccessPermission() { + assertThrows(ObjectNotFoundException.class, () -> translationPlugin.hasAccessPermission(1, null)); + PortletInstance portletInstance = mock(PortletInstance.class); + when(portletInstanceService.getPortletInstance(1)).thenReturn(portletInstance); + assertTrue(translationPlugin.hasAccessPermission(1, null)); + assertTrue(translationPlugin.hasAccessPermission(1, username)); + + String permissionExpression = "A Permission Expression"; + when(portletInstance.getPermissions()).thenReturn(Collections.singletonList(permissionExpression)); + assertFalse(translationPlugin.hasAccessPermission(1, username)); + + when(layoutAclService.isMemberOf(username, permissionExpression)).thenReturn(true); + assertTrue(translationPlugin.hasAccessPermission(1, username)); + } + + @Test + @SneakyThrows + public void getAudienceId() { + assertEquals(0l, translationPlugin.getAudienceId(0l)); + } + + @Test + @SneakyThrows + public void getSpaceId() { + assertEquals(0l, translationPlugin.getSpaceId(0l)); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java index 05d5f6b2a..79d06d64c 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java @@ -74,7 +74,7 @@ @ExtendWith(MockitoExtension.class) public class PageTemplateRestTest { - private static final String REST_PATH = "/pageTemplates"; // NOSONAR + private static final String REST_PATH = "/page/templates"; // NOSONAR private static final String SIMPLE_USER = "simple"; diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java new file mode 100644 index 000000000..8db39df3f --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java @@ -0,0 +1,264 @@ +/** + * 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.layout.rest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.spring.web.security.PortalAuthenticationManager; +import io.meeds.spring.web.security.WebSecurityConfiguration; + +import jakarta.servlet.Filter; +import lombok.SneakyThrows; + +@SpringBootTest(classes = { PortletInstanceCategoryRest.class, PortalAuthenticationManager.class, }) +@ContextConfiguration(classes = { WebSecurityConfiguration.class }) +@AutoConfigureWebMvc +@AutoConfigureMockMvc(addFilters = false) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceCategoryRestTest { + + private static final String REST_PATH = "/portlet/instance/categories"; // NOSONAR + + private static final String SIMPLE_USER = "simple"; + + private static final String TEST_PASSWORD = "testPassword"; + + static final ObjectMapper OBJECT_MAPPER; + + static { + // Workaround when Jackson is defined in shared library with different + // version and without artifact jackson-datatype-jsr310 + OBJECT_MAPPER = JsonMapper.builder() + .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private SecurityFilterChain filterChain; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChain.getFilters().toArray(new Filter[0])) + .build(); + } + + @Test + void getPortletInstanceCategorysAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void getPortletInstanceCategorysWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(portletInstanceService).getPortletInstanceCategories(any(), anyBoolean()); + } + + @Test + void getPortletInstanceCategoryAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void getPortletInstanceCategoryWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(portletInstanceService).getPortletInstanceCategory(eq(1l), any(), eq(true)); + } + + @Test + void createPortletInstanceCategoryAnonymously() throws Exception { + ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PortletInstanceCategory())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void createPortletInstanceCategoryWithUser() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(portletInstanceCategory)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + verify(portletInstanceService).createPortletInstanceCategory(portletInstanceCategory, SIMPLE_USER); + } + + @Test + void createPortletInstanceCategoryWithUserForbidden() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + when(portletInstanceService.createPortletInstanceCategory(portletInstanceCategory, + SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(portletInstanceCategory)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePortletInstanceCategoryAnonymously() throws Exception { + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PortletInstanceCategory())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void updatePortletInstanceCategoryWithUser() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstanceCategory)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + + portletInstanceCategory.setId(1l); + verify(portletInstanceService).updatePortletInstanceCategory(portletInstanceCategory, SIMPLE_USER); + } + + @Test + void updatePortletInstanceCategoryWithUserForbidden() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + portletInstanceCategory.setId(1l); + when(portletInstanceService.updatePortletInstanceCategory(portletInstanceCategory, + SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstanceCategory)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePortletInstanceCategoryWithUserNotFound() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + portletInstanceCategory.setId(1l); + when(portletInstanceService.updatePortletInstanceCategory(portletInstanceCategory, + SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstanceCategory)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isNotFound()); + } + + @Test + void deletePortletInstanceCategoryAnonymously() throws Exception { + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void deletePortletInstanceCategoryWithUser() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + + portletInstanceCategory.setId(1l); + verify(portletInstanceService).deletePortletInstanceCategory(1l, SIMPLE_USER); + } + + @Test + void deletePortletInstanceCategoryWithUserForbidden() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + portletInstanceCategory.setId(1l); + doThrow(IllegalAccessException.class).when(portletInstanceService).deletePortletInstanceCategory(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void deletePortletInstanceCategoryWithUserNotFound() throws Exception { + PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); + portletInstanceCategory.setId(1l); + doThrow(ObjectNotFoundException.class).when(portletInstanceService).deletePortletInstanceCategory(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + private RequestPostProcessor testSimpleUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("users")); + } + + @SneakyThrows + public static String asJsonString(final Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java new file mode 100644 index 000000000..8d8893f3b --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java @@ -0,0 +1,265 @@ +/** + * 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.layout.rest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.spring.web.security.PortalAuthenticationManager; +import io.meeds.spring.web.security.WebSecurityConfiguration; + +import jakarta.servlet.Filter; +import lombok.SneakyThrows; + +@SpringBootTest(classes = { PortletInstanceRest.class, PortalAuthenticationManager.class, }) +@ContextConfiguration(classes = { WebSecurityConfiguration.class }) +@AutoConfigureWebMvc +@AutoConfigureMockMvc(addFilters = false) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceRestTest { + + private static final String REST_PATH = "/portlet/instances"; // NOSONAR + + private static final String SIMPLE_USER = "simple"; + + private static final String TEST_PASSWORD = "testPassword"; + + static final ObjectMapper OBJECT_MAPPER; + + static { + // Workaround when Jackson is defined in shared library with different + // version and without artifact jackson-datatype-jsr310 + OBJECT_MAPPER = JsonMapper.builder() + .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private SecurityFilterChain filterChain; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChain.getFilters().toArray(new Filter[0])) + .build(); + } + + @Test + void getPortletInstancesAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void getPortletInstancesWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(portletInstanceService).getPortletInstances(anyLong(), any(), anyBoolean()); + } + + @Test + void getPortletInstanceAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void getPortletInstanceWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(portletInstanceService).getPortletInstance(eq(1l), any(), eq(true)); + } + + @Test + void createPortletInstanceAnonymously() throws Exception { + ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PortletInstance())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void createPortletInstanceWithUser() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(portletInstance)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + verify(portletInstanceService).createPortletInstance(portletInstance, SIMPLE_USER); + } + + @Test + void createPortletInstanceWithUserForbidden() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + when(portletInstanceService.createPortletInstance(portletInstance, + SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(portletInstance)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePortletInstanceAnonymously() throws Exception { + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PortletInstance())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void updatePortletInstanceWithUser() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstance)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + + portletInstance.setId(1l); + verify(portletInstanceService).updatePortletInstance(portletInstance, SIMPLE_USER); + } + + @Test + void updatePortletInstanceWithUserForbidden() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + portletInstance.setId(1l); + when(portletInstanceService.updatePortletInstance(portletInstance, + SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstance)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePortletInstanceWithUserNotFound() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + portletInstance.setId(1l); + when(portletInstanceService.updatePortletInstance(portletInstance, + SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(portletInstance)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isNotFound()); + } + + @Test + void deletePortletInstanceAnonymously() throws Exception { + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletInstanceService); + } + + @Test + void deletePortletInstanceWithUser() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + + portletInstance.setId(1l); + verify(portletInstanceService).deletePortletInstance(1l, SIMPLE_USER); + } + + @Test + void deletePortletInstanceWithUserForbidden() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + portletInstance.setId(1l); + doThrow(IllegalAccessException.class).when(portletInstanceService).deletePortletInstance(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void deletePortletInstanceWithUserNotFound() throws Exception { + PortletInstance portletInstance = new PortletInstance(); + portletInstance.setId(1l); + doThrow(ObjectNotFoundException.class).when(portletInstanceService).deletePortletInstance(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + private RequestPostProcessor testSimpleUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("users")); + } + + @SneakyThrows + public static String asJsonString(final Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java new file mode 100644 index 000000000..f67798b2d --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java @@ -0,0 +1,149 @@ +/** + * 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.layout.rest; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import io.meeds.layout.service.PortletService; +import io.meeds.spring.web.security.PortalAuthenticationManager; +import io.meeds.spring.web.security.WebSecurityConfiguration; + +import jakarta.servlet.Filter; +import lombok.SneakyThrows; + +@SpringBootTest(classes = { PortletRest.class, PortalAuthenticationManager.class, }) +@ContextConfiguration(classes = { WebSecurityConfiguration.class }) +@AutoConfigureWebMvc +@AutoConfigureMockMvc(addFilters = false) +@ExtendWith(MockitoExtension.class) +public class PortletRestTest { + + private static final String REST_PATH = "/portlets"; // NOSONAR + + private static final String SIMPLE_USER = "simple"; + + private static final String TEST_PASSWORD = "testPassword"; + + static final ObjectMapper OBJECT_MAPPER; + + static { + // Workaround when Jackson is defined in shared library with different + // version and without artifact jackson-datatype-jsr310 + OBJECT_MAPPER = JsonMapper.builder() + .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + @MockBean + private PortletService portletService; + + @Autowired + private SecurityFilterChain filterChain; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChain.getFilters().toArray(new Filter[0])) + .build(); + } + + @Test + void getPortletsAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletService); + } + + @Test + void getPortletsWithContributor() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testContributorUser())); + response.andExpect(status().isOk()); + verify(portletService).getPortlets(); + } + + @Test + void getPortletsWithAdmin() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testAdminUser())); + response.andExpect(status().isOk()); + verify(portletService).getPortlets(); + } + + @Test + void getPortletsWithSimple() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testSimpleUser())); + response.andExpect(status().isForbidden()); + verifyNoInteractions(portletService); + } + + private RequestPostProcessor testContributorUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("web-contributors")); + } + + private RequestPostProcessor testAdminUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("administrators")); + } + + private RequestPostProcessor testSimpleUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("users")); + } + + @SneakyThrows + public static String asJsonString(final Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/service/LayoutAclServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/LayoutAclServiceTest.java index 23181d863..d4f4c88da 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/LayoutAclServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/LayoutAclServiceTest.java @@ -18,8 +18,8 @@ */ package io.meeds.layout.service; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; @@ -39,6 +39,7 @@ import org.exoplatform.portal.mop.service.LayoutService; import org.exoplatform.services.security.Authenticator; import org.exoplatform.services.security.IdentityRegistry; +import org.exoplatform.social.core.manager.IdentityManager; @SpringBootTest(classes = { LayoutAclService.class }) @ExtendWith(MockitoExtension.class) @@ -59,6 +60,9 @@ public class LayoutAclServiceTest { @MockBean private Authenticator authenticator; + @MockBean + private IdentityManager identityManager; + @Mock private IdentityRegistry identityRegistry; diff --git a/layout-service/src/test/java/io/meeds/layout/service/NavigationLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/NavigationLayoutServiceTest.java index beaa056d3..397d4b0b5 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/NavigationLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/NavigationLayoutServiceTest.java @@ -18,9 +18,9 @@ */ package io.meeds.layout.service; -import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java index b99570052..0851595c5 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/PageLayoutServiceTest.java @@ -18,9 +18,9 @@ */ package io.meeds.layout.service; -import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -70,11 +70,15 @@ @ExtendWith(MockitoExtension.class) public class PageLayoutServiceTest { - private static final String TEST_USER = "testuser"; + private static final String ADDON_CONTAINER = "addonContainer"; - private static final SiteKey SITE_KEY = SiteKey.portal("test"); + private static final String TEST_ADDON_CONTAINER = "testAddonContainer"; - private static final PageKey PAGE_KEY = PageKey.parse("portal::test::test"); + private static final String TEST_USER = "testuser"; + + private static final SiteKey SITE_KEY = SiteKey.portal("test"); + + private static final PageKey PAGE_KEY = PageKey.parse("portal::test::test"); @MockBean private LayoutService layoutService; @@ -163,11 +167,11 @@ public void getPageLayout() throws IllegalAccessException, ObjectNotFoundExcepti public void getPageLayoutWithDynamicContainer() { when(layoutService.getPage(PAGE_KEY)).thenReturn(page); Container dynamicContainer = mock(Container.class); - when(dynamicContainer.getFactoryId()).thenReturn("addonContainer"); - when(dynamicContainer.getName()).thenReturn("testAddonContainer"); + when(dynamicContainer.getFactoryId()).thenReturn(ADDON_CONTAINER); + when(dynamicContainer.getName()).thenReturn(TEST_ADDON_CONTAINER); when(page.getChildren()).thenReturn(new ArrayList<>(Collections.singleton(dynamicContainer))); Application application = mock(Application.class); - when(addOnService.getApplications("testAddonContainer")).thenReturn(Collections.singletonList(application)); + when(addOnService.getApplications(TEST_ADDON_CONTAINER)).thenReturn(Collections.singletonList(application)); pageLayoutService.getPageLayout(PAGE_KEY); verify(dynamicContainer).setChildren(argThat(children -> children != null && children.size() == 1 && children.get(0) == application)); @@ -237,11 +241,11 @@ public void clonePage() throws IllegalAccessException, ObjectNotFoundException { public void clonePageWithDynamicContainer() throws IllegalAccessException, ObjectNotFoundException { when(layoutService.getPage(PAGE_KEY)).thenReturn(page); Container dynamicContainer = mock(Container.class); - when(dynamicContainer.getFactoryId()).thenReturn("addonContainer"); - when(dynamicContainer.getName()).thenReturn("testAddonContainer"); + when(dynamicContainer.getFactoryId()).thenReturn(ADDON_CONTAINER); + when(dynamicContainer.getName()).thenReturn(TEST_ADDON_CONTAINER); when(page.getChildren()).thenReturn(new ArrayList<>(Collections.singleton(dynamicContainer))); Application application = mock(Application.class); - when(addOnService.getApplications("testAddonContainer")).thenReturn(Collections.singletonList(application)); + when(addOnService.getApplications(TEST_ADDON_CONTAINER)).thenReturn(Collections.singletonList(application)); when(aclService.canEditPage(PAGE_KEY, TEST_USER)).thenReturn(true); when(page.getName()).thenReturn(PAGE_KEY.getName()); diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java index 2486b4362..79a5d9a07 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java @@ -18,11 +18,13 @@ */ package io.meeds.layout.service; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; @@ -48,14 +50,14 @@ import org.exoplatform.social.attachment.AttachmentService; import io.meeds.layout.model.PageTemplate; -import io.meeds.layout.plugin.PageTemplateAttachmentPlugin; -import io.meeds.layout.plugin.PageTemplateTranslationPlugin; +import io.meeds.layout.plugin.attachment.PageTemplateAttachmentPlugin; +import io.meeds.layout.plugin.translation.PageTemplateTranslationPlugin; import io.meeds.layout.storage.PageTemplateStorage; import io.meeds.social.translation.model.TranslationField; import io.meeds.social.translation.service.TranslationService; @SpringBootTest(classes = { - PageTemplateService.class, + PageTemplateService.class, }) @ExtendWith(MockitoExtension.class) public class PageTemplateServiceTest { diff --git a/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceServiceTest.java new file mode 100644 index 000000000..ab25ad00a --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceServiceTest.java @@ -0,0 +1,533 @@ +/** + * 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.layout.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.resources.LocaleConfig; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstanceCategory; +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.attachment.PortletInstanceAttachmentPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceCategoryTranslationPlugin; +import io.meeds.layout.plugin.translation.PortletInstanceTranslationPlugin; +import io.meeds.layout.storage.PortletInstanceCategoryStorage; +import io.meeds.layout.storage.PortletInstanceStorage; +import io.meeds.social.translation.model.TranslationField; +import io.meeds.social.translation.service.TranslationService; + +@SpringBootTest(classes = { PortletInstanceService.class, }) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceServiceTest { + + private static final String DESCRIPTION = "testDescription"; + + private static final String TITLE = "testTitle"; + + private static final String CONTENT_ID = "test/portlet"; + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private TranslationService translationService; + + @MockBean + private AttachmentService attachmentService; + + @MockBean + private LocaleConfigService localeConfigService; + + @MockBean + private PortletInstanceCategoryStorage portletInstanceCategoryStorage; + + @MockBean + private PortletInstanceStorage portletInstanceStorage; + + @Mock + private PortletInstanceCategory portletInstanceCategory; + + @Mock + private PortletInstance portletInstance; + + @Mock + private LocaleConfig defaultLocaleConfig; + + @Mock + private LocaleConfig localeConfig; + + @Autowired + private PortletInstanceService portletInstanceService; + + private String testuser = "testuser"; + + @Test + public void getPortletInstances() { + when(portletInstance.getId()).thenReturn(2l); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + when(portletInstanceStorage.getPortletInstances()).thenReturn(Collections.singletonList(portletInstance)); + List portletInstances = portletInstanceService.getPortletInstances(); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(portletInstance.getId(), portletInstances.get(0).getId()); + assertEquals(portletInstance.getContentId(), portletInstances.get(0).getContentId()); + assertNull(portletInstances.get(0).getName()); + assertNull(portletInstances.get(0).getDescription()); + assertEquals(0l, portletInstances.get(0).getIllustrationId()); + } + + @Test + public void getPortletInstancesByCategoryId() { + when(portletInstance.getId()).thenReturn(2l); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + when(portletInstanceStorage.getPortletInstances(3l)).thenReturn(Collections.singletonList(portletInstance)); + List portletInstances = portletInstanceService.getPortletInstances(3l, null, false); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(portletInstance.getId(), portletInstances.get(0).getId()); + assertEquals(portletInstance.getContentId(), portletInstances.get(0).getContentId()); + assertNull(portletInstances.get(0).getName()); + assertNull(portletInstances.get(0).getDescription()); + assertEquals(0l, portletInstances.get(0).getIllustrationId()); + } + + @Test + public void getPortletInstancesWithExpand() throws ObjectNotFoundException { + PortletInstance template = newPortletInstance(); + when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); + when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); + + when(portletInstanceStorage.getPortletInstances()).thenReturn(Collections.singletonList(template)); + + List portletInstances = portletInstanceService.getPortletInstances(true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(template.getId(), portletInstances.get(0).getId()); + assertEquals(template.getContentId(), portletInstances.get(0).getContentId()); + assertNull(portletInstances.get(0).getName()); + assertNull(portletInstances.get(0).getDescription()); + assertEquals(template.getIllustrationId(), portletInstances.get(0).getIllustrationId()); + + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME)).thenThrow(ObjectNotFoundException.class); + portletInstances = portletInstanceService.getPortletInstances(true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + + reset(translationService); + + TranslationField titleTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME)).thenReturn(titleTranslationField); + portletInstances = portletInstanceService.getPortletInstances(true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(template.getId(), portletInstances.get(0).getId()); + assertEquals(template.getContentId(), portletInstances.get(0).getContentId()); + assertNull(portletInstances.get(0).getName()); + assertNull(portletInstances.get(0).getDescription()); + assertEquals(template.getIllustrationId(), portletInstances.get(0).getIllustrationId()); + + String frTitle = TITLE; + when(titleTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.FRENCH, frTitle)); + + portletInstances = portletInstanceService.getPortletInstances(true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(frTitle, portletInstances.get(0).getName()); + + TranslationField descriptionTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.DESCRIPTION_FIELD_NAME)).thenReturn(descriptionTranslationField); + String enDesc = DESCRIPTION; + when(descriptionTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.ENGLISH, enDesc)); + + portletInstances = portletInstanceService.getPortletInstances(true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(enDesc, portletInstances.get(0).getDescription()); + + when(attachmentService.getAttachmentFileIds(PortletInstanceAttachmentPlugin.OBJECT_TYPE, + "2")).thenReturn(Collections.singletonList("32")); + portletInstances = portletInstanceService.getPortletInstances(Locale.GERMAN, true); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(32l, portletInstances.get(0).getIllustrationId()); + } + + @Test + public void getPortletInstanceWithExpand() throws ObjectNotFoundException { + PortletInstance template = newPortletInstance(); + when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); + when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); + + when(portletInstanceStorage.getPortletInstance(template.getId())).thenReturn(template); + + PortletInstance retrievedPortletInstance = portletInstanceService.getPortletInstance(template.getId()); + assertNotNull(retrievedPortletInstance); + assertEquals(template.getId(), retrievedPortletInstance.getId()); + assertEquals(template.getContentId(), retrievedPortletInstance.getContentId()); + assertEquals(template.getIllustrationId(), retrievedPortletInstance.getIllustrationId()); + + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME)).thenThrow(ObjectNotFoundException.class); + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l, Locale.FRENCH, true); + assertNotNull(retrievedPortletInstance); + + reset(translationService); + + TranslationField titleTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.TITLE_FIELD_NAME)).thenReturn(titleTranslationField); + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l, Locale.FRENCH, true); + assertNotNull(retrievedPortletInstance); + assertEquals(template.getId(), retrievedPortletInstance.getId()); + assertEquals(template.getContentId(), retrievedPortletInstance.getContentId()); + assertNull(retrievedPortletInstance.getName()); + assertNull(retrievedPortletInstance.getDescription()); + assertEquals(template.getIllustrationId(), retrievedPortletInstance.getIllustrationId()); + + String frTitle = TITLE; + when(titleTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.FRENCH, frTitle)); + + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l, Locale.FRENCH, true); + assertEquals(frTitle, retrievedPortletInstance.getName()); + + TranslationField descriptionTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceTranslationPlugin.OBJECT_TYPE, + template.getId(), + PortletInstanceTranslationPlugin.DESCRIPTION_FIELD_NAME)).thenReturn(descriptionTranslationField); + String enDesc = DESCRIPTION; + when(descriptionTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.ENGLISH, enDesc)); + + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l, Locale.ENGLISH, true); + assertNotNull(retrievedPortletInstance); + assertEquals(enDesc, retrievedPortletInstance.getDescription()); + + when(attachmentService.getAttachmentFileIds(PortletInstanceAttachmentPlugin.OBJECT_TYPE, + "2")).thenReturn(Collections.singletonList("32")); + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l, Locale.GERMAN, true); + assertNotNull(retrievedPortletInstance); + assertEquals(32l, retrievedPortletInstance.getIllustrationId()); + } + + @Test + public void getPortletInstance() { + when(portletInstanceStorage.getPortletInstance(2l)).thenReturn(portletInstance); + PortletInstance retrievedPortletInstance = portletInstanceService.getPortletInstance(3l); + assertNull(retrievedPortletInstance); + + retrievedPortletInstance = portletInstanceService.getPortletInstance(2l); + assertNotNull(retrievedPortletInstance); + assertEquals(portletInstance, retrievedPortletInstance); + } + + @Test + public void createPortletInstance() throws IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.createPortletInstance(portletInstance, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + portletInstanceService.createPortletInstance(portletInstance, testuser); + verify(portletInstanceStorage, times(1)).createPortletInstance(portletInstance); + } + + @Test + public void deletePortletInstance() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstance(2l, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceService.deletePortletInstance(2l, testuser)); + + when(portletInstanceStorage.getPortletInstance(2l)).thenReturn(portletInstance); + when(portletInstance.isSystem()).thenReturn(true); + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstance(2l, testuser)); + + when(portletInstance.isSystem()).thenReturn(false); + portletInstanceService.deletePortletInstance(2l, testuser); + + verify(attachmentService, times(1)).deleteAttachments(PortletInstanceAttachmentPlugin.OBJECT_TYPE, "2"); + verify(translationService, times(1)).deleteTranslationLabels(PortletInstanceTranslationPlugin.OBJECT_TYPE, 2l); + verify(portletInstanceStorage, times(1)).deletePortletInstance(2l); + } + + @Test + public void deletePortletInstanceWhenException() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstance(2l, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + when(portletInstanceStorage.getPortletInstance(2l)).thenReturn(portletInstance); + doThrow(RuntimeException.class).when(attachmentService).deleteAttachments(anyString(), any()); + doThrow(ObjectNotFoundException.class).when(translationService).deleteTranslationLabels(anyString(), anyLong()); + portletInstanceService.deletePortletInstance(2l, testuser); + verify(portletInstanceStorage, times(1)).deletePortletInstance(2l); + } + + @Test + public void updatePortletInstance() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.updatePortletInstance(portletInstance, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + portletInstanceService.updatePortletInstance(portletInstance, testuser); + verify(portletInstanceStorage, times(1)).updatePortletInstance(portletInstance); + } + + @Test + public void getPortletInstanceCategories() { + when(portletInstanceCategory.getId()).thenReturn(2l); + when(portletInstanceCategory.getIcon()).thenReturn(CONTENT_ID); + + when(portletInstanceCategoryStorage.getPortletInstanceCategories()).thenReturn(Collections.singletonList(portletInstanceCategory)); + List portletInstanceCategories = portletInstanceService.getPortletInstanceCategories(); + assertNotNull(portletInstanceCategories); + assertEquals(1, portletInstanceCategories.size()); + assertEquals(portletInstanceCategory.getId(), portletInstanceCategories.get(0).getId()); + assertEquals(portletInstanceCategory.getIcon(), portletInstanceCategories.get(0).getIcon()); + assertNull(portletInstanceCategories.get(0).getName()); + assertNull(portletInstanceCategories.get(0).getDescription()); + } + + @Test + public void getPortletInstanceCategoriesWithExpand() throws ObjectNotFoundException { + PortletInstanceCategory category = newPortletInstanceCategory(); + when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); + when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); + + when(portletInstanceCategoryStorage.getPortletInstanceCategories()).thenReturn(Collections.singletonList(category)); + + List portletInstanceCategorys = portletInstanceService.getPortletInstanceCategories(); + assertNotNull(portletInstanceCategorys); + assertEquals(1, portletInstanceCategorys.size()); + assertEquals(category.getId(), portletInstanceCategorys.get(0).getId()); + assertEquals(category.getIcon(), portletInstanceCategorys.get(0).getIcon()); + assertNull(portletInstanceCategorys.get(0).getName()); + assertNull(portletInstanceCategorys.get(0).getDescription()); + + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME)).thenThrow(ObjectNotFoundException.class); + portletInstanceCategorys = portletInstanceService.getPortletInstanceCategories(Locale.ENGLISH, true); + assertNotNull(portletInstanceCategorys); + assertEquals(1, portletInstanceCategorys.size()); + + reset(translationService); + + TranslationField titleTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME)).thenReturn(titleTranslationField); + portletInstanceCategorys = portletInstanceService.getPortletInstanceCategories(Locale.FRENCH, true); + assertNotNull(portletInstanceCategorys); + assertEquals(1, portletInstanceCategorys.size()); + assertEquals(category.getId(), portletInstanceCategorys.get(0).getId()); + assertEquals(category.getIcon(), portletInstanceCategorys.get(0).getIcon()); + assertNull(portletInstanceCategorys.get(0).getName()); + assertNull(portletInstanceCategorys.get(0).getDescription()); + + String frTitle = TITLE; + when(titleTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.FRENCH, frTitle)); + + portletInstanceCategorys = portletInstanceService.getPortletInstanceCategories(Locale.FRENCH, true); + assertNotNull(portletInstanceCategorys); + assertEquals(1, portletInstanceCategorys.size()); + assertEquals(frTitle, portletInstanceCategorys.get(0).getName()); + + TranslationField descriptionTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.DESCRIPTION_FIELD_NAME)).thenReturn(descriptionTranslationField); + String enDesc = DESCRIPTION; + when(descriptionTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.ENGLISH, enDesc)); + + portletInstanceCategorys = portletInstanceService.getPortletInstanceCategories(Locale.ENGLISH, true); + assertNotNull(portletInstanceCategorys); + assertEquals(1, portletInstanceCategorys.size()); + assertEquals(enDesc, portletInstanceCategorys.get(0).getDescription()); + } + + @Test + public void getPortletInstanceCategoryWithExpand() throws ObjectNotFoundException { + PortletInstanceCategory category = newPortletInstanceCategory(); + when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); + when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); + + when(portletInstanceCategoryStorage.getPortletInstanceCategory(category.getId())).thenReturn(category); + + PortletInstanceCategory retrievedPortletInstanceCategory = + portletInstanceService.getPortletInstanceCategory(category.getId()); + assertNotNull(retrievedPortletInstanceCategory); + assertEquals(category.getId(), retrievedPortletInstanceCategory.getId()); + assertEquals(category.getIcon(), retrievedPortletInstanceCategory.getIcon()); + + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME)).thenThrow(ObjectNotFoundException.class); + retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(category.getId(), Locale.FRENCH, true); + assertNotNull(retrievedPortletInstanceCategory); + + reset(translationService); + + TranslationField titleTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.TITLE_FIELD_NAME)).thenReturn(titleTranslationField); + retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(category.getId(), Locale.FRENCH, true); + assertNotNull(retrievedPortletInstanceCategory); + assertEquals(category.getId(), retrievedPortletInstanceCategory.getId()); + assertEquals(category.getIcon(), retrievedPortletInstanceCategory.getIcon()); + assertNull(retrievedPortletInstanceCategory.getName()); + assertNull(retrievedPortletInstanceCategory.getDescription()); + + String frTitle = TITLE; + when(titleTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.FRENCH, frTitle)); + + retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(category.getId(), Locale.FRENCH, true); + assertEquals(frTitle, retrievedPortletInstanceCategory.getName()); + + TranslationField descriptionTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, + category.getId(), + PortletInstanceCategoryTranslationPlugin.DESCRIPTION_FIELD_NAME)).thenReturn(descriptionTranslationField); + String enDesc = DESCRIPTION; + when(descriptionTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.ENGLISH, enDesc)); + + retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(category.getId(), Locale.ENGLISH, true); + assertNotNull(retrievedPortletInstanceCategory); + assertEquals(enDesc, retrievedPortletInstanceCategory.getDescription()); + } + + @Test + public void getPortletInstanceCategory() { + when(portletInstanceCategoryStorage.getPortletInstanceCategory(2l)).thenReturn(portletInstanceCategory); + PortletInstanceCategory retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(3l); + assertNull(retrievedPortletInstanceCategory); + + retrievedPortletInstanceCategory = portletInstanceService.getPortletInstanceCategory(2l); + assertNotNull(retrievedPortletInstanceCategory); + assertEquals(portletInstanceCategory, retrievedPortletInstanceCategory); + } + + @Test + public void createPortletInstanceCategory() throws IllegalAccessException { + assertThrows(IllegalAccessException.class, + () -> portletInstanceService.createPortletInstanceCategory(portletInstanceCategory, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + portletInstanceService.createPortletInstanceCategory(portletInstanceCategory, testuser); + verify(portletInstanceCategoryStorage, times(1)).createPortletInstanceCategory(portletInstanceCategory); + } + + @Test + public void deletePortletInstanceCategory() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstanceCategory(2l, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceService.deletePortletInstanceCategory(2l, testuser)); + + when(portletInstanceCategoryStorage.getPortletInstanceCategory(2l)).thenReturn(portletInstanceCategory); + when(portletInstanceCategory.isSystem()).thenReturn(true); + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstanceCategory(2l, testuser)); + + when(portletInstanceCategory.isSystem()).thenReturn(false); + portletInstanceService.deletePortletInstanceCategory(2l, testuser); + + verify(translationService, times(1)).deleteTranslationLabels(PortletInstanceCategoryTranslationPlugin.OBJECT_TYPE, 2l); + verify(portletInstanceCategoryStorage, times(1)).deletePortletInstanceCategory(2l); + } + + @Test + public void deletePortletInstanceCategoryWhenException() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, () -> portletInstanceService.deletePortletInstanceCategory(2l, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + when(portletInstanceCategoryStorage.getPortletInstanceCategory(2l)).thenReturn(portletInstanceCategory); + doThrow(ObjectNotFoundException.class).when(translationService).deleteTranslationLabels(anyString(), anyLong()); + portletInstanceService.deletePortletInstanceCategory(2l, testuser); + verify(portletInstanceCategoryStorage, times(1)).deletePortletInstanceCategory(2l); + } + + @Test + public void updatePortletInstanceCategory() throws ObjectNotFoundException, IllegalAccessException { + assertThrows(IllegalAccessException.class, + () -> portletInstanceService.updatePortletInstanceCategory(portletInstanceCategory, testuser)); + + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + portletInstanceService.updatePortletInstanceCategory(portletInstanceCategory, testuser); + verify(portletInstanceCategoryStorage, times(1)).updatePortletInstanceCategory(portletInstanceCategory); + } + + private PortletInstanceCategory newPortletInstanceCategory() { + return new PortletInstanceCategory(3l, + null, + null, + "icon", + true, + Collections.singletonList("permission")); + } + + private PortletInstance newPortletInstance() { + return new PortletInstance(2l, + "name", + "description", + 5l, + CONTENT_ID, + Collections.singletonList(new PortletInstancePreference("prefName", "prefValue")), + 7l, + Collections.singletonList("permission"), + Collections.singletonList("edit"), + true, + false, + true); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java index 3294276fd..5c1ae9dc3 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java @@ -54,7 +54,7 @@ import lombok.SneakyThrows; @SpringBootTest(classes = { - SiteLayoutService.class, + SiteLayoutService.class, }) @ExtendWith(MockitoExtension.class) public class SiteLayoutServiceTest { diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/injection/PageTemplateImportServiceTest.java similarity index 68% rename from layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java rename to layout-service/src/test/java/io/meeds/layout/service/injection/PageTemplateImportServiceTest.java index f5e0100cd..b8cd97515 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/injection/PageTemplateImportServiceTest.java @@ -16,7 +16,7 @@ * 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.layout.service; +package io.meeds.layout.service.injection; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -29,50 +29,39 @@ import org.exoplatform.commons.api.settings.SettingService; import org.exoplatform.container.configuration.ConfigurationManager; -import org.exoplatform.services.resources.LocaleConfigService; import org.exoplatform.services.resources.ResourceBundleService; import org.exoplatform.social.attachment.AttachmentService; -import io.meeds.layout.plugin.PageTemplateAttachmentPlugin; -import io.meeds.layout.plugin.PageTemplateTranslationPlugin; -import io.meeds.social.translation.service.TranslationService; +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PageTemplateService; @SpringBootTest(classes = { PageTemplateImportService.class }) @ExtendWith(MockitoExtension.class) public class PageTemplateImportServiceTest { @MockBean - private LayoutAclService layoutAclService; + private LayoutAclService layoutAclService; @MockBean - private TranslationService translationService; + private LayoutTranslationImportService layoutTranslationImportService; @MockBean - private AttachmentService attachmentService; + private AttachmentService attachmentService; @MockBean - private LocaleConfigService localeConfigService; + private PageTemplateService pageTemplateService; @MockBean - private PageTemplateService pageTemplateService; + private SettingService settingService; @MockBean - private SettingService settingService; + private ResourceBundleService resourceBundleService; @MockBean - private ResourceBundleService resourceBundleService; - - @MockBean - private ConfigurationManager configurationManager; - - @MockBean - private PageTemplateAttachmentPlugin pageTemplateAttachmentPlugin; - - @MockBean - private PageTemplateTranslationPlugin pageTemplateTranslationPlugin; + private ConfigurationManager configurationManager; @Autowired - private PageTemplateImportService pageTemplateImportService; + private PageTemplateImportService pageTemplateImportService; @Test public void init() { @@ -81,4 +70,5 @@ public void init() { assertDoesNotThrow(() -> pageTemplateImportService.importPageTemplates(), "Shouldn't stop the container initialization if page templates fails"); } + } diff --git a/layout-service/src/test/java/io/meeds/layout/service/injection/PortletInstanceImportServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/injection/PortletInstanceImportServiceTest.java new file mode 100644 index 000000000..1bcdba182 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/service/injection/PortletInstanceImportServiceTest.java @@ -0,0 +1,74 @@ +/** + * 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.layout.service.injection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.api.settings.SettingService; +import org.exoplatform.container.configuration.ConfigurationManager; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.service.LayoutAclService; +import io.meeds.layout.service.PortletInstanceService; +import io.meeds.layout.service.PortletService; + +@SpringBootTest(classes = { PortletInstanceImportService.class }) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceImportServiceTest { + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private LayoutTranslationImportService layoutTranslationService; + + @MockBean + private AttachmentService attachmentService; + + @MockBean + private PortletInstanceService portletInstanceService; + + @MockBean + private PortletService portletService; + + @MockBean + private SettingService settingService; + + @MockBean + private ConfigurationManager configurationManager; + + @Autowired + private PortletInstanceImportService portletInstanceImportService; + + @Test + public void init() { + assertDoesNotThrow(() -> portletInstanceImportService.init(), + "Shouldn't stop the container initialization if portlet instances import fails"); + assertDoesNotThrow(() -> portletInstanceImportService.importPortletInstances(), + "Shouldn't stop the container initialization if portlet instances import fails"); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/storage/PageTemplateStorageTest.java b/layout-service/src/test/java/io/meeds/layout/storage/PageTemplateStorageTest.java index 81d369ceb..3a1603ce1 100644 --- a/layout-service/src/test/java/io/meeds/layout/storage/PageTemplateStorageTest.java +++ b/layout-service/src/test/java/io/meeds/layout/storage/PageTemplateStorageTest.java @@ -18,8 +18,8 @@ */ package io.meeds.layout.storage; -import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -45,7 +45,7 @@ import io.meeds.layout.model.PageTemplate; @SpringBootTest(classes = { - PageTemplateStorage.class, + PageTemplateStorage.class, }) @ExtendWith(MockitoExtension.class) public class PageTemplateStorageTest { diff --git a/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceCategoryStorageTest.java b/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceCategoryStorageTest.java new file mode 100644 index 000000000..50aa46d1e --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceCategoryStorageTest.java @@ -0,0 +1,142 @@ +/** + * 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.layout.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.dao.PortletInstanceCategoryDAO; +import io.meeds.layout.entity.PortletInstanceCategoryEntity; +import io.meeds.layout.model.PortletInstanceCategory; + +@SpringBootTest(classes = { + PortletInstanceCategoryStorage.class, +}) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceCategoryStorageTest { + + private static final String ICON = "icon"; + + @MockBean + private PortletInstanceCategoryDAO portletInstanceCategoryDAO; + + @Mock + private PortletInstanceCategoryEntity portletInstanceCategoryEntity; + + @Mock + private PortletInstanceCategory portletInstanceCategory; + + @Autowired + private PortletInstanceCategoryStorage portletInstanceCategoryStorage; + + String username = "test"; + + @Test + public void getPortletInstanceCategories() { + when(portletInstanceCategoryDAO.findAll()).thenReturn(Collections.singletonList(portletInstanceCategoryEntity)); + when(portletInstanceCategoryEntity.getId()).thenReturn(2l); + when(portletInstanceCategoryEntity.getIcon()).thenReturn(ICON); + when(portletInstanceCategoryEntity.isSystem()).thenReturn(true); + when(portletInstanceCategoryEntity.getPermissions()).thenReturn(Collections.singletonList("permissions")); + + List portletInstanceCategories = portletInstanceCategoryStorage.getPortletInstanceCategories(); + assertNotNull(portletInstanceCategories); + assertEquals(1, portletInstanceCategories.size()); + assertEquals(portletInstanceCategoryEntity.getId(), portletInstanceCategories.get(0).getId()); + assertEquals(portletInstanceCategoryEntity.getIcon(), portletInstanceCategories.get(0).getIcon()); + assertEquals(portletInstanceCategoryEntity.getPermissions(), portletInstanceCategories.get(0).getPermissions()); + assertEquals(portletInstanceCategoryEntity.isSystem(), portletInstanceCategories.get(0).isSystem()); + } + + @Test + public void getPortletInstanceCategory() { + when(portletInstanceCategoryDAO.findById(2l)).thenReturn(Optional.of(portletInstanceCategoryEntity)); + when(portletInstanceCategoryEntity.getId()).thenReturn(2l); + when(portletInstanceCategoryEntity.getIcon()).thenReturn(ICON); + + PortletInstanceCategory retrievedPortletInstanceCategory = portletInstanceCategoryStorage.getPortletInstanceCategory(2l); + assertEquals(portletInstanceCategoryEntity.getId(), retrievedPortletInstanceCategory.getId()); + assertEquals(portletInstanceCategoryEntity.getIcon(), retrievedPortletInstanceCategory.getIcon()); + } + + @Test + public void createPortletInstanceCategory() { + when(portletInstanceCategory.getIcon()).thenReturn(ICON); + + when(portletInstanceCategoryDAO.save(any(PortletInstanceCategoryEntity.class))).thenAnswer(invocation -> { + PortletInstanceCategoryEntity entity = invocation.getArgument(0); + entity.setId(2l); + return entity; + }); + PortletInstanceCategory createdPortletInstanceCategory = + portletInstanceCategoryStorage.createPortletInstanceCategory(portletInstanceCategory); + assertNotNull(createdPortletInstanceCategory); + assertEquals(2l, createdPortletInstanceCategory.getId()); + assertEquals(portletInstanceCategory.getIcon(), createdPortletInstanceCategory.getIcon()); + } + + @Test + public void updatePortletInstanceCategory() throws ObjectNotFoundException { + when(portletInstanceCategory.getIcon()).thenReturn(ICON); + + when(portletInstanceCategoryDAO.save(any(PortletInstanceCategoryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceCategoryStorage.updatePortletInstanceCategory(portletInstanceCategory)); + + when(portletInstanceCategory.getId()).thenReturn(2l); + when(portletInstanceCategory.getIcon()).thenReturn(ICON); + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceCategoryStorage.updatePortletInstanceCategory(portletInstanceCategory)); + + when(portletInstanceCategoryDAO.existsById(portletInstanceCategory.getId())).thenReturn(true); + PortletInstanceCategory updatedPortletInstanceCategory = + portletInstanceCategoryStorage.updatePortletInstanceCategory(portletInstanceCategory); + assertNotNull(updatedPortletInstanceCategory); + assertEquals(portletInstanceCategory.getId(), updatedPortletInstanceCategory.getId()); + assertEquals(portletInstanceCategory.getIcon(), updatedPortletInstanceCategory.getIcon()); + } + + @Test + public void deletePortletInstanceCategory() throws ObjectNotFoundException { + assertThrows(ObjectNotFoundException.class, () -> portletInstanceCategoryStorage.deletePortletInstanceCategory(2l)); + when(portletInstanceCategoryDAO.existsById(2l)).thenReturn(true); + portletInstanceCategoryStorage.deletePortletInstanceCategory(2l); + verify(portletInstanceCategoryDAO, times(1)).deleteById(2l); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceStorageTest.java b/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceStorageTest.java new file mode 100644 index 000000000..097ae962e --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/storage/PortletInstanceStorageTest.java @@ -0,0 +1,145 @@ +/** + * 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.layout.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.dao.PortletInstanceDAO; +import io.meeds.layout.entity.PortletInstanceEntity; +import io.meeds.layout.model.PortletInstance; + +@SpringBootTest(classes = { + PortletInstanceStorage.class, +}) +@ExtendWith(MockitoExtension.class) +public class PortletInstanceStorageTest { + + private static final String CONTENT_ID = "test/portlet"; + + @MockBean + private PortletInstanceDAO portletInstanceDAO; + + @MockBean + private PortletStorage portletStorage; + + @Mock + private PortletInstanceEntity portletInstanceEntity; + + @Mock + private PortletInstance portletInstance; + + @Autowired + private PortletInstanceStorage portletInstanceStorage; + + String username = "test"; + + @Test + public void getPortletInstances() { + when(portletInstanceDAO.findAll()).thenReturn(Collections.singletonList(portletInstanceEntity)); + when(portletInstanceEntity.getId()).thenReturn(2l); + when(portletInstanceEntity.getContentId()).thenReturn(CONTENT_ID); + when(portletInstanceEntity.isSystem()).thenReturn(true); + when(portletInstanceEntity.getPermissions()).thenReturn(Collections.singletonList("permissions")); + + List portletInstances = portletInstanceStorage.getPortletInstances(); + assertNotNull(portletInstances); + assertEquals(1, portletInstances.size()); + assertEquals(portletInstanceEntity.getId(), portletInstances.get(0).getId()); + assertEquals(portletInstanceEntity.getContentId(), portletInstances.get(0).getContentId()); + assertEquals(portletInstanceEntity.getPermissions(), portletInstances.get(0).getPermissions()); + assertEquals(portletInstanceEntity.isSystem(), portletInstances.get(0).isSystem()); + } + + @Test + public void getPortletInstance() { + when(portletInstanceDAO.findById(2l)).thenReturn(Optional.of(portletInstanceEntity)); + when(portletInstanceEntity.getId()).thenReturn(2l); + when(portletInstanceEntity.getContentId()).thenReturn(CONTENT_ID); + + PortletInstance retrievedPortletInstance = portletInstanceStorage.getPortletInstance(2l); + assertEquals(portletInstanceEntity.getId(), retrievedPortletInstance.getId()); + assertEquals(portletInstanceEntity.getContentId(), retrievedPortletInstance.getContentId()); + } + + @Test + public void createPortletInstance() { + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + when(portletInstanceDAO.save(any(PortletInstanceEntity.class))).thenAnswer(invocation -> { + PortletInstanceEntity entity = invocation.getArgument(0); + entity.setId(2l); + return entity; + }); + PortletInstance createdPortletInstance = + portletInstanceStorage.createPortletInstance(portletInstance); + assertNotNull(createdPortletInstance); + assertEquals(2l, createdPortletInstance.getId()); + assertEquals(portletInstance.getContentId(), createdPortletInstance.getContentId()); + } + + @Test + public void updatePortletInstance() throws ObjectNotFoundException { + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + when(portletInstanceDAO.save(any(PortletInstanceEntity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceStorage.updatePortletInstance(portletInstance)); + + when(portletInstance.getId()).thenReturn(2l); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceStorage.updatePortletInstance(portletInstance)); + + when(portletInstanceDAO.existsById(portletInstance.getId())).thenReturn(true); + PortletInstance updatedPortletInstance = + portletInstanceStorage.updatePortletInstance(portletInstance); + assertNotNull(updatedPortletInstance); + assertEquals(portletInstance.getId(), updatedPortletInstance.getId()); + assertEquals(portletInstance.getContentId(), updatedPortletInstance.getContentId()); + } + + @Test + public void deletePortletInstance() throws ObjectNotFoundException { + assertThrows(ObjectNotFoundException.class, () -> portletInstanceStorage.deletePortletInstance(2l)); + when(portletInstanceDAO.existsById(2l)).thenReturn(true); + portletInstanceStorage.deletePortletInstance(2l); + verify(portletInstanceDAO, times(1)).deleteById(2l); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/storage/PortletStorageTest.java b/layout-service/src/test/java/io/meeds/layout/storage/PortletStorageTest.java new file mode 100644 index 000000000..5e03f2a96 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/storage/PortletStorageTest.java @@ -0,0 +1,152 @@ +/** + * 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.layout.storage; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; + +import org.gatein.common.i18n.LocalizedString; +import org.gatein.pc.api.Portlet; +import org.gatein.pc.api.PortletInvoker; +import org.gatein.pc.api.info.CapabilitiesInfo; +import org.gatein.pc.api.info.MetaInfo; +import org.gatein.pc.api.info.ModeInfo; +import org.gatein.pc.api.info.PortletInfo; +import org.gatein.pc.api.info.PreferencesInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import io.meeds.layout.model.PortletDescriptor; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { PortletStorage.class, }) +@ExtendWith(MockitoExtension.class) +public class PortletStorageTest { + + private static final String MODE = "edit"; + + private static final String NAME = "name"; + + private static final String APPLICATION_NAME = "test"; + + private static final String PORTLET_NAME = "portlet"; + + @MockBean + private PortletInvoker portletInvoker; + + @Mock + private PortletInfo portletInfo; + + @Mock + private MetaInfo metaInfo; + + @Mock + private Portlet portlet; + + @Mock + private CapabilitiesInfo capabilitiesInfo; + + @Mock + private ModeInfo modeInfo; + + @Mock + private PreferencesInfo preferencesInfo; + + @Mock + private LocalizedString nameLS; + + @Mock + private LocalizedString descriptionLS; + + @Autowired + private PortletStorage portletStorage; + + @BeforeEach + public void init() { + portletStorage.setPortletDescriptors(null); + } + + @Test + @SneakyThrows + public void getPortletDescriptors() { + when(portletInvoker.getPortlets()).thenReturn(Collections.singleton(portlet)); + when(portlet.getInfo()).thenReturn(portletInfo); + when(portletInfo.getApplicationName()).thenReturn(APPLICATION_NAME); + when(portletInfo.getName()).thenReturn(PORTLET_NAME); + when(portletInfo.getMeta()).thenReturn(metaInfo); + when(portletInfo.getCapabilities()).thenReturn(capabilitiesInfo); + when(capabilitiesInfo.getModes(org.gatein.common.net.media.MediaType.create("text/html"))).thenReturn(Collections.singleton(modeInfo)); + when(modeInfo.getModeName()).thenReturn(MODE); + when(metaInfo.getMetaValue(MetaInfo.DISPLAY_NAME)).thenReturn(nameLS); + when(metaInfo.getMetaValue(MetaInfo.DESCRIPTION)).thenReturn(descriptionLS); + when(nameLS.getDefaultString()).thenReturn(NAME); + + List portletDescriptors = portletStorage.getPortletDescriptors(); + assertNotNull(portletDescriptors); + assertEquals(1, portletDescriptors.size()); + + PortletDescriptor portletDescriptor = portletDescriptors.get(0); + assertEquals(APPLICATION_NAME, portletDescriptor.getApplicationName()); + assertEquals(PORTLET_NAME, portletDescriptor.getPortletName()); + assertEquals(APPLICATION_NAME + "/" + PORTLET_NAME, portletDescriptor.getContentId()); + assertEquals(PORTLET_NAME, portletDescriptor.getDescription()); + assertEquals(NAME, portletDescriptor.getName()); + assertEquals(Collections.singletonList(MODE), portletDescriptor.getSupportedModes()); + } + + @Test + @SneakyThrows + public void getPortletDescriptor() { + when(portletInvoker.getPortlets()).thenReturn(Collections.singleton(portlet)); + when(portlet.getInfo()).thenReturn(portletInfo); + when(portletInfo.getApplicationName()).thenReturn(APPLICATION_NAME); + when(portletInfo.getName()).thenReturn(PORTLET_NAME); + when(portletInfo.getMeta()).thenReturn(metaInfo); + when(portletInfo.getCapabilities()).thenReturn(capabilitiesInfo); + when(capabilitiesInfo.getModes(org.gatein.common.net.media.MediaType.create("text/html"))).thenReturn(Collections.singleton(modeInfo)); + when(modeInfo.getModeName()).thenReturn(MODE); + when(metaInfo.getMetaValue(MetaInfo.DISPLAY_NAME)).thenReturn(nameLS); + when(metaInfo.getMetaValue(MetaInfo.DESCRIPTION)).thenReturn(descriptionLS); + when(nameLS.getDefaultString()).thenReturn(NAME); + + PortletDescriptor portletDescriptor = portletStorage.getPortletDescriptor(PORTLET_NAME); + assertEquals(APPLICATION_NAME, portletDescriptor.getApplicationName()); + assertEquals(PORTLET_NAME, portletDescriptor.getPortletName()); + assertEquals(APPLICATION_NAME + "/" + PORTLET_NAME, portletDescriptor.getContentId()); + assertEquals(PORTLET_NAME, portletDescriptor.getDescription()); + assertEquals(NAME, portletDescriptor.getName()); + assertEquals(Collections.singletonList(MODE), portletDescriptor.getSupportedModes()); + + when(portlet.isRemote()).thenReturn(true); + portletStorage.setPortletDescriptors(null); + assertNull(portletStorage.getPortletDescriptor(PORTLET_NAME)); + } +} diff --git a/layout-service/src/test/resources/conf/portal/configuration.xml b/layout-service/src/test/resources/conf/portal/configuration.xml new file mode 100644 index 000000000..8bf7a3fbb --- /dev/null +++ b/layout-service/src/test/resources/conf/portal/configuration.xml @@ -0,0 +1,42 @@ + + + + + + org.exoplatform.commons.api.persistence.DataInitializer + + PortalRDBMSChangeLogsPlugin + addChangeLogsPlugin + org.exoplatform.commons.persistence.impl.ChangeLogsPlugin + + + changelogs + Change logs of Portal RDBMS + db/changelog/portal-rdbms.db.changelog-1.0.0.xml + + + + + + diff --git a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties index b68309a54..e73d6a142 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -177,5 +177,44 @@ portlets.label.editProperties=Edit Properties portlets.label.duplicate=Duplicate portlets.label.delete=Delete portlets.label.system.noDelete=This portlet instance cannot be deleted +portlets.label.category.system.noDelete=This category cannot be deleted portlets.label.preview=Preview of {0} portlets.label.openIllustrationPreview=Open illustration Preview + +layout.portletInstance.MembersPortlet.name=Members +layout.portletInstance.MembersPortlet.description=Members Portlet +layout.portletInstance.SpaceSettingPortlet.name=Space Setting +layout.portletInstance.SpaceSettingPortlet.description=Space Setting Portlet +layout.portletInstance.Image.name=Image +layout.portletInstance.Image.description=Image Portlet +layout.portletInstance.ExternalSpacesList.name=External Spaces list +layout.portletInstance.ExternalSpacesList.description=External Spaces list Portlet +layout.portletInstance.SpaceBannerPortlet.name=Space Banner +layout.portletInstance.SpaceBannerPortlet.description=Space Banner Portlet +layout.portletInstance.WhoIsOnLinePortlet.name=Who Is OnLine +layout.portletInstance.WhoIsOnLinePortlet.description=Who Is OnLine Portlet +layout.portletInstance.PeopleOverview.name=People Overview +layout.portletInstance.PeopleOverview.description=People Overview Portlet +layout.portletInstance.SpaceActivityStreamPortlet.name=Space Activity Stream +layout.portletInstance.SpaceActivityStreamPortlet.description=Space Activity Stream Portlet +layout.portletInstance.SuggestionsPeopleAndSpace.name=People and Space Suggestions +layout.portletInstance.SuggestionsPeopleAndSpace.description=People and Space Suggestions Portlet +layout.portletInstance.SpacesOverview.name=Spaces Overview +layout.portletInstance.SpacesOverview.description=Spaces Overview Portlet +layout.portletInstance.SpaceInfos.name=Space Infos +layout.portletInstance.SpaceInfos.description=Space Infos Portlet +layout.portletInstance.VerticalMenu.name=Site Vertical Menu +layout.portletInstance.VerticalMenu.description=Site Vertical Menu Portlet +layout.portletInstance.Breadcrumb.name=Site Vertical Menu +layout.portletInstance.Breadcrumb.description=Site Vertical Menu Portlet +layout.portletInstance.Links.name=Links +layout.portletInstance.Links.description=Links Portlet +layout.portletInstance.category.all.name=All +layout.portletInstance.category.spaces.name=Space +layout.portletInstance.category.spaces.description=Spaces applications +layout.portletInstance.category.content.name=Content +layout.portletInstance.category.content.description=Content Applications +layout.portletInstance.category.tools.name=Tools +layout.portletInstance.category.tools.description=Tools +layout.portletInstance.category.navigation.name=Navigation +layout.portletInstance.category.navigation.description=Applications for Navigation diff --git a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml index e8e5a99a5..13672bd49 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml @@ -73,8 +73,8 @@ en locale.portlet.LayoutEditor - site navigation - site navigation Management + Layout Editor + Layout Editor Management diff --git a/layout-webapp/src/main/webapp/images/portlets/DefaultPortlet.png b/layout-webapp/src/main/webapp/images/portlets/DefaultPortlet.png new file mode 100644 index 0000000000000000000000000000000000000000..bb21b8d377871e81e3a4a335b2103b4bedf8ce2b GIT binary patch literal 3643 zcmV-B4#e?^P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000AKNkl4xO7{`D5>Z$P}N(d>IZiNn=lnngu>_dSW;s1QO#$g+E}aA z0?-;a3 zB%l+{bR36zy&g+=&1N%LH)gn<`MW-f z%cEi2_DBaNy*Mzg3S0-u`sWJ3y9Ov7>42Jq1Is{l_&iD-;9&S1{SY3Ls?E=jcfFom zfA+N&7Jz{rI3|@HSKG0XaqmQuh%@m0IaSNDc9l||N)F4i#_Ha%gfJHa|L!doi)6Fe zktYr}{pC=F@VFd!;opH|G8s>s{JSW*LQ@A`IBuYe@bW!E2`~g9EOlV49TRy}exn@G&Pm&4e)nEYHr)lF4L7p4jjAuNnh>=1gk{du|K_ zoinvb#7|8Z_Bv2*Dv1MQuon(`p<%v1Eg>u+EJ;Kh=yjv7>86ek2X-dyz)2>NgU!uN z?Z$wgIaBLE{I#|2?LTI+21*$tNhHz=nneEmf$&NU`K{c*!ZW~Q(=<6dJHxW9nr+*) zIMFi<1Jg8_nVA7N_5<)}d3m|Fx3`ybT{qM{xjRV?$t0#}vb40+1CDMi5pId|4DbSY z2b5yE40eH6z(=633s59wi~tmX0#GCX1)u;FfFc1X00p1`6#b*$0{~xJ37-8b60!gQ N002ovPDHLkV1m5Zy%hie literal 0 HcmV?d00001 diff --git a/layout-webapp/src/main/webapp/vue-app/common-illustration/components/Illustration.vue b/layout-webapp/src/main/webapp/vue-app/common-illustration/components/Illustration.vue index 2a1037e93..f74a89ec6 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-illustration/components/Illustration.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-illustration/components/Illustration.vue @@ -36,7 +36,8 @@ + max-width="60" + contain /> diff --git a/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceCategoryService.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceCategoryService.js new file mode 100644 index 000000000..4f4a6cba4 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceCategoryService.js @@ -0,0 +1,87 @@ +/* + * 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. + */ + +export function getPortletInstanceCategories() { + return fetch('/layout/rest/portlet/instance/categories', { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlet instance categories'); + } else { + return resp.json(); + } + }); +} + +export function getPortletInstanceCategory(id) { + return fetch(`/layout/rest/portlet/instance/categories/${id}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlet instance category'); + } else { + return resp.json(); + } + }); +} + +export function createPortletInstanceCategory(category) { + return fetch('/layout/rest/portlet/instance/categories', { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(category), + }).then((resp) => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when creating portlet instance categories'); + } + }); +} + +export function updatePortletInstanceCategory(category) { + return fetch(`/layout/rest/portlet/instance/categories/${category.id}`, { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(category), + }).then((resp) => { + if (!resp?.ok) { + throw new Error('Error when updating portlet instance categories'); + } + }); +} + +export function deletePortletInstanceCategory(id) { + return fetch(`/layout/rest/portlet/instance/categories/${id}`, { + credentials: 'include', + method: 'DELETE', + }).then((resp) => { + if (!resp?.ok) { + throw new Error('Error when deleting portlet instance categories'); + } + }); +} diff --git a/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js new file mode 100644 index 000000000..496fa61c9 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js @@ -0,0 +1,87 @@ +/* + * 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. + */ + +export function getPortletInstances(categoryId) { + return fetch(`/layout/rest/portlet/instances${categoryId && `?categoryId=${categoryId}` || ''}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlet instances'); + } else { + return resp.json(); + } + }); +} + +export function getPortletInstance(id) { + return fetch(`/layout/rest/portlet/instances/${id}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlet instance'); + } else { + return resp.json(); + } + }); +} + +export function createPortletInstance(portletInstance) { + return fetch('/layout/rest/portlet/instances', { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(portletInstance), + }).then((resp) => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when creating portlet instance'); + } + }); +} + +export function updatePortletInstance(portletInstance) { + return fetch(`/layout/rest/portlet/instances/${portletInstance.id}`, { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(portletInstance), + }).then((resp) => { + if (!resp?.ok) { + throw new Error('Error when updating portlet instance'); + } + }); +} + +export function deletePortletInstance(id) { + return fetch(`/layout/rest/portlet/instances/${id}`, { + credentials: 'include', + method: 'DELETE', + }).then((resp) => { + if (!resp?.ok) { + throw new Error('Error when deleting portlet instance'); + } + }); +} diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/PortletService.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletService.js similarity index 76% rename from layout-webapp/src/main/webapp/vue-app/common/js/PortletService.js rename to layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletService.js index 25dcc7d30..23599cd56 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/PortletService.js +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletService.js @@ -18,21 +18,14 @@ */ export function getPortlets() { - return Promise.resolve(); -} - -export function getPortlet() { - return Promise.resolve(); -} - -export function createPortlet() { - return Promise.resolve(); -} - -export function updatePortlet() { - return Promise.resolve(); -} - -export function deletePortlet() { - return Promise.resolve(); + return fetch('/layout/rest/portlets', { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlets'); + } else { + return resp.json(); + } + }); } diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/PortletInstanceService.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/main.js similarity index 71% rename from layout-webapp/src/main/webapp/vue-app/common/js/PortletInstanceService.js rename to layout-webapp/src/main/webapp/vue-app/common-portlets/main.js index e586eb2e4..d50c812a1 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/PortletInstanceService.js +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/main.js @@ -17,22 +17,5 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export function getPortletInstances() { - return Promise.resolve(); -} - -export function getPortletInstance() { - return Promise.resolve(); -} - -export function createPortletInstance() { - return Promise.resolve(); -} - -export function updatePortletInstance() { - return Promise.resolve(); -} - -export function deletePortletInstance() { - return Promise.resolve(); -} +import './initComponents.js'; +import './services.js'; diff --git a/layout-webapp/src/main/webapp/vue-app/common-portlets/services.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/services.js new file mode 100644 index 000000000..bdbcc3c25 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/services.js @@ -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. + */ + +import * as portletInstanceCategoryService from './js/PortletInstanceCategoryService.js'; +import * as portletInstanceService from './js/PortletInstanceService.js'; +import * as portletService from './js/PortletService.js'; + +if (!Vue.prototype.$portletInstanceCategoryService) { + window.Object.defineProperty(Vue.prototype, '$portletInstanceCategoryService', { + value: portletInstanceCategoryService, + }); +} +if (!Vue.prototype.$portletInstanceService) { + window.Object.defineProperty(Vue.prototype, '$portletInstanceService', { + value: portletInstanceService, + }); +} +if (!Vue.prototype.$portletService) { + window.Object.defineProperty(Vue.prototype, '$portletService', { + value: portletService, + }); +} diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/PageTemplateService.js b/layout-webapp/src/main/webapp/vue-app/common/js/PageTemplateService.js index 616435865..2947684aa 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/PageTemplateService.js +++ b/layout-webapp/src/main/webapp/vue-app/common/js/PageTemplateService.js @@ -18,7 +18,7 @@ */ export function getPageTemplates() { - return fetch('/layout/rest/pageTemplates', { + return fetch('/layout/rest/page/templates', { method: 'GET', credentials: 'include', }).then(resp => { @@ -31,7 +31,7 @@ export function getPageTemplates() { } export function getPageTemplate(id) { - return fetch(`/layout/rest/pageTemplates/${id}`, { + return fetch(`/layout/rest/page/templates/${id}`, { method: 'GET', credentials: 'include', }).then(resp => { @@ -44,7 +44,7 @@ export function getPageTemplate(id) { } export function createPageTemplate(pageContent, disabled) { - return fetch('/layout/rest/pageTemplates', { + return fetch('/layout/rest/page/templates', { credentials: 'include', method: 'POST', headers: { @@ -64,7 +64,7 @@ export function createPageTemplate(pageContent, disabled) { } export function updatePageTemplate(pageTemplate) { - return fetch(`/layout/rest/pageTemplates/${pageTemplate.id}`, { + return fetch(`/layout/rest/page/templates/${pageTemplate.id}`, { credentials: 'include', method: 'PUT', headers: { @@ -79,7 +79,7 @@ export function updatePageTemplate(pageTemplate) { } export function deletePageTemplate(id) { - return fetch(`/layout/rest/pageTemplates/${id}`, { + return fetch(`/layout/rest/page/templates/${id}`, { credentials: 'include', method: 'DELETE', }).then((resp) => { diff --git a/layout-webapp/src/main/webapp/vue-app/common/services.js b/layout-webapp/src/main/webapp/vue-app/common/services.js index 5b6171490..3b4ca7c56 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/services.js +++ b/layout-webapp/src/main/webapp/vue-app/common/services.js @@ -21,8 +21,6 @@ import * as siteLayoutService from './js/SiteLayoutService.js'; import * as navigationLayoutService from './js/NavigationLayoutService.js'; import * as pageLayoutService from './js/PageLayoutService.js'; import * as pageTemplateService from './js/PageTemplateService.js'; -import * as portletInstanceService from './js/PortletInstanceService.js'; -import * as portletService from './js/PortletService.js'; import * as applicationUtils from './js/ApplicationUtils.js'; if (!Vue.prototype.$navigationLayoutService) { @@ -49,17 +47,6 @@ if (!Vue.prototype.$pageTemplateService) { }); } -if (!Vue.prototype.$portletInstanceService) { - window.Object.defineProperty(Vue.prototype, '$portletInstanceService', { - value: portletInstanceService, - }); -} -if (!Vue.prototype.$portletService) { - window.Object.defineProperty(Vue.prototype, '$portletService', { - value: portletService, - }); -} - if (!Vue.prototype.$applicationUtils) { window.Object.defineProperty(Vue.prototype, '$applicationUtils', { value: applicationUtils, diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue index 9335413cf..8643c6185 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue @@ -32,16 +32,16 @@ max-width="45" @error="displayDefault = true" /> -
+
- {{ applicationName }} + :title="name" + class="text-truncate subtitle-1 px-1 pt-2 text-color ApplicationCardTitle"> + {{ name }}
- {{ applicationDescription || applicationName }} + :title="description" + class="text-truncate subtitle-2 px-1 pt-0 pb-2 text-sub-title ApplicationCardDescription"> + {{ description || name }}
@@ -72,18 +72,18 @@ export default { }), computed: { imgSrc() { - return this.displayDefault && this.defaultImageSrc || `/${this.webApplicationName}/skin/DefaultSkin/portletIcons/${this.portletName}.png`; + return this.displayDefault && this.defaultImageSrc || `/${this.applicationName}/skin/DefaultSkin/portletIcons/${this.portletName}.png`; }, - webApplicationName() { - return this.application?.contentId?.split?.('/')?.[0]; + applicationName() { + return this.application?.applicationName || this.application?.contentId?.split?.('/')?.[0]; }, portletName() { - return this.application?.contentId?.split?.('/')?.[1]; + return this.application?.portletName || this.application?.contentId?.split?.('/')?.[1]; }, - applicationName() { - return this.application?.displayName || this.application?.applicationName; + name() { + return this.application?.name; }, - applicationDescription() { + description() { return this.application?.description; }, }, diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue index 23feac509..a372994af 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue @@ -22,16 +22,16 @@ - +
@@ -49,16 +49,26 @@ export default { type: Object, default: null, }, + applications: { + type: Array, + default: null, + }, }, computed: { categoryName() { - return this.category.label; + return this.category?.name; + }, + categoryLabel() { + return this.category?.label || this.category?.name; + }, + categoryId() { + return this.category?.id; }, - applications() { - return this.category?.applications || []; + categoryApplications() { + return this.applications?.filter?.(a => (a.categoryId && a.categoryId === this.categoryId) || (!a.categoryId && a.applicationName === this.categoryName)) || []; }, hasApplications() { - return this.applications.length > 0; + return this.categoryApplications.length > 0; }, }, }; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue index 6df28502b..eb476eba6 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue @@ -44,12 +44,14 @@ v-for="(category, index) in applicationCategories" :key="category.id" :category="category" + :applications="applications" :expanded="expanded === index" @addApplication="addApplication" /> @@ -85,20 +87,20 @@ export default { return applicationCategories; }, applications() { - return this.applicationCategories.flatMap(c => c.applications); + return this.$root.allApplications; }, otherApplications() { return this.allApplications.filter(a => !this.applications.find(app => app.contentId === a.contentId)); }, otherCategories() { return this.otherApplications.reduce((otherCategories, application) => { - const category = otherCategories.find(c => c.name === application.categoryName); + const category = otherCategories.find(c => c.name === application.applicationName); if (category) { category.applications.push(application); } else { otherCategories.push({ - name: application.categoryName, - label: `${this.$t('layout.otherApplications')}: ${this.$te(`layout.${application.categoryName}`) ? this.$t(`layout.${application.categoryName}`) : application.categoryName}`, + name: application.applicationName, + label: `${this.$t('layout.otherApplications')}: ${this.$te(`layout.${application.applicationName}`) ? this.$t(`layout.${application.applicationName}`) : application.applicationName}`, applications: [application], }); } @@ -120,7 +122,7 @@ export default { }, loadMore() { this.$refs.drawer.startLoading(); - this.$applicationRegistryService.getApplications('supportedModes') + this.$portletService.getPortlets() .then(applications => this.allApplications = applications) .finally(() => { this.canLoadMore = false; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js b/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js index b21f80bcf..76d244629 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js @@ -20,6 +20,7 @@ import './initComponents.js'; import '../common/initComponents.js'; import '../common-page-template/initComponents.js'; +import '../common-portlets/main.js'; import './extensions.js'; import './services.js'; @@ -144,9 +145,9 @@ export function init() { document.addEventListener('extension-layout-editor-container-updated', this.refreshContainerTypes); document.addEventListener('drawerOpened', this.setDrawerOpened); document.addEventListener('drawerClosed', this.setDrawerClosed); - this.$applicationRegistryService.getCategories('supportedModes') + this.$portletInstanceCategoryService.getPortletInstanceCategories() .then(categories => this.applicationCategories = categories); - this.$applicationRegistryService.getApplications('supportedModes') + this.$portletInstanceService.getPortletInstances() .then(applications => this.allApplications = applications); this.$brandingService.getBrandingInformation() .then(data => this.branding = data); diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/PortletsManagement.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/PortletsManagement.vue index 53f1e6e97..24b59a4f7 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/PortletsManagement.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/PortletsManagement.vue @@ -20,21 +20,26 @@ --> @@ -46,7 +46,7 @@ export default { data: () => ({ portlets: null, - tabName: 'portlets', + tabName: 'instances', }), }; diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Categories.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Categories.vue new file mode 100644 index 000000000..c7bd4fa33 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Categories.vue @@ -0,0 +1,62 @@ + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryItem.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryItem.vue new file mode 100644 index 000000000..afdb91d9c --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryItem.vue @@ -0,0 +1,38 @@ + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryMenu.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryMenu.vue new file mode 100644 index 000000000..bd596f866 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/CategoryMenu.vue @@ -0,0 +1,125 @@ + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Item.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Item.vue index cdeaeb33f..d9a0e4e24 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Item.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Item.vue @@ -7,8 +7,8 @@ width="70px"> + object-type="portletInstance" + default-src="/layout/images/portlets/DefaultPortlet.png" /> ({ portletInstances: [], portletInstanceToDelete: null, + categoryId: 0, loading: false, collator: new Intl.Collator(eXo.env.portal.language, {numeric: true, sensitivity: 'base'}), }), @@ -128,6 +129,7 @@ export default { this.$root.$on('portlets-instance-disabled', this.refreshPortletInstances); this.$root.$on('portlets-instance-saved', this.refreshPortletInstances); this.$root.$on('portlets-instance-delete', this.deletePortletInstanceConfirm); + this.$root.$on('portlets-instance-category-selected', this.selectCategoryId); this.refreshPortletInstances(); }, beforeDestroy() { @@ -163,9 +165,15 @@ export default { this.$refs.deleteConfirmDialog.open(); } }, + selectCategoryId(id) { + if (this.categoryId !== id) { + this.categoryId = id; + this.refreshPortletInstances(); + } + }, refreshPortletInstances() { this.loading = true; - return this.$portletInstanceService.getPortletInstances() + return this.$portletInstanceService.getPortletInstances(this.categoryId) .then(portletInstances => this.portletInstances = portletInstances || []) .finally(() => this.loading = false); }, diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Main.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Main.vue new file mode 100644 index 000000000..f94a4fb77 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Main.vue @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue index 1747dbedb..f4c770c65 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue @@ -170,7 +170,7 @@ export default { document.addEventListener('click', this.closeMenuOnClick); }, beforeDestroy() { - this.$root.$off('pportlets-instance-menu-opened', this.checkMenuStatus); + this.$root.$off('portlets-instance-menu-opened', this.checkMenuStatus); document.removeEventListener('click', this.closeMenuOnClick); }, methods: { diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/Item.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/Item.vue index 7ab64c7f4..6b1156c46 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/Item.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/Item.vue @@ -5,26 +5,23 @@ v-if="!$root.isMobile" align="center" width="70px"> - + - ({ menu: false, hoverMenu: false, + defaultIllustrationSrc: '/layout/images/portlets/DefaultPortlet.png', + illustrationSrc: null, }), computed: { portletId() { @@ -60,12 +59,6 @@ export default { description() { return this.$te(this.portlet?.description) ? this.$t(this.portlet?.description) : this.portlet?.description; }, - illustrationId() { - return this.portlet?.illustrationId; - }, - illustrationSrc() { - return this.illustrationId && `${eXo.env.portal.context}/${eXo.env.portal.rest}/v1/social/attachments/portlets/${this.portletId}/${this.illustrationId}` || '/layout/images/portlets/DefaultPreview.webp'; - }, }, watch: { hoverMenu() { @@ -78,5 +71,8 @@ export default { } }, }, + created() { + this.illustrationSrc = `/${this.portlet.applicationName}/skin/DefaultSkin/portletIcons/${this.portlet.portletName}.png`; + }, }; \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/List.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/List.vue index 1b8bceaaa..22e4dafe2 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/List.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/portlets/List.vue @@ -17,14 +17,6 @@ :portlet="props.item" /> -
diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/initComponents.js b/layout-webapp/src/main/webapp/vue-app/portlets/initComponents.js index 8a3f2ac5c..3566775ec 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/initComponents.js +++ b/layout-webapp/src/main/webapp/vue-app/portlets/initComponents.js @@ -21,6 +21,11 @@ import PortletsManagement from './components/PortletsManagement.vue'; import Toolbar from './components/header/Toolbar.vue'; +import InstanceMain from './components/instances/Main.vue'; +import InstanceCategories from './components/instances/Categories.vue'; +import InstanceCategoryMenu from './components/instances/CategoryMenu.vue'; +import InstanceCategoryItem from './components/instances/CategoryItem.vue'; + import InstanceList from './components/instances/List.vue'; import InstanceItem from './components/instances/Item.vue'; import InstanceMenu from './components/instances/Menu.vue'; @@ -34,6 +39,12 @@ const components = { 'portlets-toolbar': Toolbar, + 'portlets-instance-main': InstanceMain, + + 'portlets-instance-categories': InstanceCategories, + 'portlets-instance-category': InstanceCategoryItem, + 'portlets-instance-category-menu': InstanceCategoryMenu, + 'portlets-instance-list': InstanceList, 'portlets-instance-item': InstanceItem, 'portlets-instance-item-menu': InstanceMenu, diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/main.js b/layout-webapp/src/main/webapp/vue-app/portlets/main.js index 1a8b27617..b212f580c 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/main.js +++ b/layout-webapp/src/main/webapp/vue-app/portlets/main.js @@ -20,6 +20,7 @@ import './initComponents.js'; import '../common/initComponents.js'; import '../common-illustration/initComponents.js'; +import '../common-portlets/main.js'; // get overridden components if exists if (extensionRegistry) {