From 9344eedb3b28833cb1c8a4c18f95df81bda74076 Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Thu, 30 May 2024 18:55:18 +0100 Subject: [PATCH 1/3] feat: Implement Portlet Instance Application and Renderer Service - MEED-6920 - Meeds-io/MIPs#139 This change will allow to dynamically display in the same page a Portlet instance selected by a request parameter. In addition, this will initialize the Portlet instance editor application. --- .../renderer/PortletInstanceAddOnPlugin.java | 64 ++++ .../PortletInstanceApplicationAdapter.java | 282 ++++++++++++++++++ .../service/PortletInstanceRenderService.java | 219 ++++++++++++++ .../service/PortletInstanceService.java | 3 +- .../PortletInstanceImportService.java | 14 +- .../locale/portlet/LayoutEditor_en.properties | 3 + .../main/webapp/WEB-INF/gatein-resources.xml | 29 ++ .../src/main/webapp/WEB-INF/portlet.xml | 21 ++ .../src/main/webapp/html/portletEditor.html | 7 + .../main/webapp/vue-app/layout-editor/main.js | 2 +- .../components/PortletEditor.vue | 38 +++ .../components/content/Content.vue | 27 ++ .../components/toolbar/Toolbar.vue | 45 +++ .../components/toolbar/actions/SaveButton.vue | 63 ++++ .../vue-app/portlet-editor/initComponents.js | 36 +++ .../webapp/vue-app/portlet-editor/main.js | 66 ++++ .../portlets/components/instances/Menu.vue | 35 +-- layout-webapp/webpack.prod.js | 1 + 18 files changed, 926 insertions(+), 29 deletions(-) create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceAddOnPlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceApplicationAdapter.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java create mode 100644 layout-webapp/src/main/webapp/html/portletEditor.html create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceAddOnPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceAddOnPlugin.java new file mode 100644 index 000000000..73a07c116 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceAddOnPlugin.java @@ -0,0 +1,64 @@ +/** + * 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.renderer; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.addons.AddOnPlugin; +import org.exoplatform.commons.addons.AddOnService; +import org.exoplatform.portal.config.model.Application; + +import jakarta.annotation.PostConstruct; + +@Component +public class PortletInstanceAddOnPlugin extends AddOnPlugin { + + private static final String PORTLET_EDITOR_DYNAMIC_CONTAINER = "portlet-editor"; + + @Autowired + private PortletInstanceApplicationAdapter portletInstanceApplicationAdapter; + + @Autowired + private AddOnService addonService; + + @PostConstruct + public void init() { + addonService.addPlugin(this); + } + + @Override + public int getPriority() { + return 1; + } + + @Override + public String getContainerName() { + return PORTLET_EDITOR_DYNAMIC_CONTAINER; + } + + @Override + public List> getApplications() { + return Collections.singletonList(portletInstanceApplicationAdapter); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceApplicationAdapter.java b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceApplicationAdapter.java new file mode 100644 index 000000000..c99e97a58 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/PortletInstanceApplicationAdapter.java @@ -0,0 +1,282 @@ +/** + * 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.renderer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.portal.application.PortalRequestContext; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.ApplicationState; +import org.exoplatform.portal.config.model.ApplicationType; +import org.exoplatform.portal.config.model.ModelStyle; +import org.exoplatform.portal.config.model.Properties; +import org.exoplatform.portal.config.serialize.PortletApplication; +import org.exoplatform.portal.pom.data.ApplicationData; +import org.exoplatform.portal.pom.spi.portlet.Portlet; + +import io.meeds.layout.service.PortletInstanceRenderService; + +import lombok.SneakyThrows; + +@Component +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class PortletInstanceApplicationAdapter extends PortletApplication { + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; + + private ThreadLocal application = new ThreadLocal<>(); + + @Override + public ModelStyle getCssStyle() { + return getApplication().getCssStyle(); + } + + @Override + public void setCssStyle(ModelStyle cssStyle) { + getApplication().setCssStyle(cssStyle); + } + + @Override + public ApplicationType getType() { + return ApplicationType.PORTLET; + } + + @Override + public String getWidth() { + return getApplication().getWidth(); + } + + @Override + public void setWidth(String s) { + getApplication().setWidth(s); + } + + @Override + public String getHeight() { + return getApplication().getHeight(); + } + + @Override + public void setHeight(String s) { + getApplication().setHeight(s); + } + + @Override + public String getId() { + return getApplication().getId(); + } + + @Override + public void setId(String value) { + getApplication().setId(value); + } + + @Override + public String[] getAccessPermissions() { + return getApplication().getAccessPermissions(); + } + + @Override + public void setAccessPermissions(String[] accessPermissions) { + getApplication().setAccessPermissions(accessPermissions); + } + + @Override + public boolean isModifiable() { + return getApplication().isModifiable(); + } + + @Override + public void setModifiable(boolean modifiable) { + getApplication().setModifiable(modifiable); + } + + @Override + public ApplicationState getState() { + return getApplication().getState(); + } + + @Override + public void setState(ApplicationState value) { + getApplication().setState(value); + } + + @Override + public boolean getShowInfoBar() { + return getApplication().getShowInfoBar(); + } + + @Override + public void setShowInfoBar(boolean b) { + getApplication().setShowInfoBar(b); + } + + @Override + public boolean getShowApplicationState() { + return getApplication().getShowApplicationState(); + } + + @Override + public void setShowApplicationState(boolean b) { + getApplication().setShowApplicationState(b); + } + + @Override + public boolean getShowApplicationMode() { + return getApplication().getShowApplicationMode(); + } + + @Override + public void setShowApplicationMode(boolean b) { + getApplication().setShowApplicationMode(b); + } + + @Override + public String getIcon() { + return getApplication().getIcon(); + } + + @Override + public void setIcon(String value) { + getApplication().setIcon(value); + } + + @Override + public String getDescription() { + return getApplication().getDescription(); + } + + @Override + public void setDescription(String des) { + getApplication().setDescription(des); + } + + @Override + public String getTitle() { + return getApplication().getTitle(); + } + + @Override + public void setTitle(String value) { + getApplication().setTitle(value); + } + + @Override + public Properties getProperties() { + return getApplication().getProperties(); + } + + @Override + public void setProperties(Properties properties) { + getApplication().setProperties(properties); + } + + @Override + public String getTheme() { + return getApplication().getTheme(); + } + + @Override + public void setTheme(String theme) { + getApplication().setTheme(theme); + } + + @Override + public String getCssClass() { + return getApplication().getCssClass(); + } + + @Override + public String getBorderColor() { + return getApplication().getBorderColor(); + } + + @Override + public ApplicationData build() { + return getApplication().build(); + } + + @Override + public void resetStorage() { + getApplication().resetStorage(); + } + + @Override + public void setCssClass(String cssClass) { + getApplication().setCssClass(cssClass); + } + + @Override + public void setBorderColor(String borderColor) { + getApplication().setBorderColor(borderColor); + } + + @Override + public String getStorageId() { + return getApplication().getStorageId(); + } + + @Override + public String getStorageName() { + return getApplication().getStorageName(); + } + + @Override + public void setStorageName(String storageName) { + getApplication().setStorageName(storageName); + } + + @SneakyThrows + public Application getApplication() { // NOSONAR + Application portletApplication = application.get(); + if (portletApplication == null) { + portletApplication = portletInstanceRenderService.getPortletInstanceApplication(getCurrentUserName(), + getPortletInstanceId(), + getApplicationStorageId()); + manageRequestCache(portletApplication); + } + return portletApplication; + } + + private void manageRequestCache(Application portletApplication) { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + if (requestContext != null) { + application.set(portletApplication); + requestContext.addOnRequestEnd(() -> application.remove()); + } + } + + private String getPortletInstanceId() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getParameter("portletInstanceId"); + } + + private String getApplicationStorageId() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getParameter("portal:componentId"); + } + + private String getCurrentUserName() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getRemoteUser(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java new file mode 100644 index 000000000..79ed571d1 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java @@ -0,0 +1,219 @@ +/** + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.gatein.pc.api.PortletInvoker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +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.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.Page; +import org.exoplatform.portal.config.model.TransientApplicationState; +import org.exoplatform.portal.config.serialize.PortletApplication; +import org.exoplatform.portal.mop.SiteKey; +import org.exoplatform.portal.mop.page.PageContext; +import org.exoplatform.portal.mop.page.PageKey; +import org.exoplatform.portal.mop.page.PageState; +import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.pom.spi.portlet.Portlet; +import org.exoplatform.portal.pom.spi.portlet.PortletBuilder; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstancePreference; + +/** + * A plugin that is used to display a selected portlet instance in the context + * of the PortletEditor page until. This should be changed to use + * {@link PortletInvoker} to process 'view' & 'edit' & 'serveResource' switch + * JSR-168 & JSR-286 requirements. But for now, to kkep WebUI based portlets + * working (which doesn't implement the JSRs portlet bridge), we will use this + * trick to allow displaying a portlet instance inside WebUI dynamic container. + */ +@Service +public class PortletInstanceRenderService { + + private static final String PORTLET_EDITOR_PORTLET_CONTENT_ID = "layout/PortletEditor"; + + private static final String PORTLET_EDITOR_SYSTEM_PAGE = "_portletEditor"; + + private static final PageKey PORTLET_EDITOR_SYSTEM_PAGE_KEY = + new PageKey(SiteKey.portal("global"), PORTLET_EDITOR_SYSTEM_PAGE); + + private static final Context CONTEXT = Context.GLOBAL.id("PORTLET_INSTANCE"); + + private static final Scope SCOPE = Scope.APPLICATION.id("PORTLET_INSTANCE_APPLICATION"); + + @Autowired + private SettingService settingService; + + @Autowired + private PortletInstanceService portletInstanceService; + + @Autowired + private LayoutService layoutService; + + private Application portletEditorApplication; + + public Application getPortletInstanceApplication(String username, // NOSONAR + String portletInstanceId, + String applicationStorageId) throws IllegalAccessException, + ObjectNotFoundException { + if (StringUtils.isNotBlank(portletInstanceId)) { + // Display the portlet instance by id + return getOrCreatePortletInstanceApplication(portletInstanceId, username); + } else if (StringUtils.isNotBlank(applicationStorageId)) { + // Display the app by storage id + return layoutService.getApplicationModel(applicationStorageId); + } else { + // Display the editor + return getPortletEditorApplication(); + } + } + + private Application getOrCreatePortletInstanceApplication(String portletInstanceId, + String userName) throws IllegalAccessException, + ObjectNotFoundException { + PortletInstance portletInstance = portletInstanceService.getPortletInstance(Long.parseLong(portletInstanceId), + userName, + Locale.ENGLISH, + false); + long applicationId = getPortletInstanceApplicationId(portletInstanceId); + if (applicationId == 0) { + return createPortletInstanceApplication(portletInstance); + } else { + try { + return layoutService.getApplicationModel(String.valueOf(applicationId)); + } catch (Exception e) { + return createPortletInstanceApplication(portletInstance); + } + } + } + + @SuppressWarnings("unchecked") + private synchronized Application createPortletInstanceApplication(PortletInstance portletInstance) { + TransientApplicationState state = new TransientApplicationState<>(portletInstance.getContentId()); + + List permissions = portletInstance.getPermissions(); + List preferences = portletInstance.getPreferences(); + if (CollectionUtils.isNotEmpty(preferences)) { + PortletBuilder builder = new PortletBuilder(); + preferences.stream().forEach(pref -> builder.add(pref.getName(), pref.getValue())); + state.setContentState(builder.build()); + } + + PortletApplication portletApplication = new PortletApplication(); + portletApplication.setState(state); + portletApplication.setAccessPermissions(CollectionUtils.isEmpty(permissions) ? new String[] { UserACL.EVERYONE } : + permissions.toArray(new String[0])); + + Page page = getPortletInstanceSystemPage(); + Container container = (Container) page.getChildren().get(0); + ArrayList children = container.getChildren(); + int index; + if (CollectionUtils.isEmpty(children)) { + index = 0; + children = new ArrayList<>(); + } else { + index = children.size(); + children = new ArrayList<>(children); + } + children.add(portletApplication); + container.setChildren(children); + page.setChildren(new ArrayList<>(Collections.singletonList(container))); + layoutService.save(page); + + container = getPortletInstanceSystemContainer(); + Application application = (Application) container.getChildren().get(index); + savePortletInstanceApplicationId(application.getStorageId(), + portletInstance.getId()); + return application; + } + + private long getPortletInstanceApplicationId(String portletInstanceId) { + SettingValue settingValue = settingService.get(CONTEXT, SCOPE, portletInstanceId); + if (settingValue != null && settingValue.getValue() != null && StringUtils.isNotBlank(settingValue.getValue().toString())) { + return Long.parseLong(settingValue.getValue().toString()); + } else { + return 0; + } + } + + private void savePortletInstanceApplicationId(String applicationStorageId, long portletInstanceId) { + settingService.set(CONTEXT, + SCOPE, + String.valueOf(portletInstanceId), + SettingValue.create(applicationStorageId)); + } + + private Container getPortletInstanceSystemContainer() { + return (Container) getPortletInstanceSystemPage().getChildren().get(0); + } + + private Page getPortletInstanceSystemPage() { + Page page = layoutService.getPage(PORTLET_EDITOR_SYSTEM_PAGE_KEY); + if (page == null) { + page = new Page(); + page.setTitle("Portlet Editor Working Page"); + page.setEditPermission("manager:/platform/administrators"); + page.setPageId(PORTLET_EDITOR_SYSTEM_PAGE_KEY.format()); + + Container container = new Container(); + container.setTemplate("nop"); + page.setChildren(new ArrayList<>(Collections.singletonList(container))); + + PageState pageState = new PageState(page.getTitle(), + page.getDescription(), + false, + null, + Arrays.asList(UserACL.EVERYONE), + page.getEditPermission(), + Arrays.asList(UserACL.EVERYONE), + Arrays.asList(UserACL.EVERYONE)); + layoutService.save(new PageContext(PORTLET_EDITOR_SYSTEM_PAGE_KEY, pageState), page); + page = layoutService.getPage(PORTLET_EDITOR_SYSTEM_PAGE_KEY); + } + return page; + } + + private Application getPortletEditorApplication() { + if (portletEditorApplication == null) { + portletEditorApplication = new PortletApplication(); + portletEditorApplication.setAccessPermissions(new String[] { UserACL.EVERYONE }); + portletEditorApplication.setState(new TransientApplicationState<>(PORTLET_EDITOR_PORTLET_CONTENT_ID)); + } + return portletEditorApplication; + } + +} 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 index 369fef0dc..539ef6ab4 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java @@ -29,6 +29,7 @@ import org.springframework.stereotype.Service; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.resources.LocaleConfigService; @@ -47,7 +48,7 @@ @Service public class PortletInstanceService { - private static final List EVERYONE_PERMISSIONS_LIST = Collections.singletonList("Everyone"); + private static final List EVERYONE_PERMISSIONS_LIST = Collections.singletonList(UserACL.EVERYONE); private static final Log LOG = ExoLogger.getLogger(PortletInstanceService.class); 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 index bd8ccbac6..bde42ea52 100644 --- 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 @@ -198,16 +198,16 @@ protected void importDescriptor(PortletInstanceDescriptor descriptor) { protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d, long oldId) { String descriptorId = d.getNameId(); - LOG.info("Importing Portlet category instance {}", descriptorId); + LOG.debug("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); + LOG.debug("Importing Portlet instance category {} title translations", descriptorId); saveCategoryNames(d, category); // Mark as imported setCategorySettingValue(descriptorId, category.getId()); } - LOG.info("Importing Portlet instance category {} finished successfully", descriptorId); + LOG.debug("Importing Portlet instance category {} finished successfully", descriptorId); } catch (Exception e) { LOG.warn("An error occurred while importing portlet instance category {}", descriptorId, e); } @@ -215,16 +215,16 @@ protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d protected void importPortletInstance(PortletInstanceDescriptor d, long oldId) { String descriptorId = d.getNameId(); - LOG.info("Importing Portlet instance {}", descriptorId); + LOG.debug("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); + LOG.debug("Importing Portlet instance {} title translations", descriptorId); saveNames(d, portletInstance); - LOG.info("Importing Portlet instance {} description translations", descriptorId); + LOG.debug("Importing Portlet instance {} description translations", descriptorId); saveDescriptions(d, portletInstance); if (StringUtils.isNotBlank(d.getIllustrationPath())) { - LOG.info("Importing Portlet instance {} illustration", descriptorId); + LOG.debug("Importing Portlet instance {} illustration", descriptorId); saveIllustration(portletInstance.getId(), d.getIllustrationPath()); } // Mark as imported 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 74fac0347..7b8ea45a6 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -178,6 +178,7 @@ portlets.menu.open=Open Menu portlets.label.instanceMenu={0} portlets.label.menu={0} portlets.label.closeMenu=Close Menu +portlets.label.editInstance=Edit Instance portlets.label.editLayout=Edit Layout portlets.label.system.noEditLayout=This portlet instance's layout cannot be updated portlets.label.editProperties=Edit Properties @@ -287,3 +288,5 @@ portlets.instancePreview=Preview portlets.uploadPreviewTitle=Upload an illustration for portlet instance portlets.label.createInstance=Create instance portlets.noPreviewAvailable=No preview available + +layout.editPortletInstance=Edit portlet instance {0} diff --git a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml index f8b312a89..5921fe7fd 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -309,5 +309,34 @@ + + PortletEditor + + portlet-editor-group + + + extensionRegistry + + + commonVueComponents + + + attachImage + + + vue + + + vuetify + + + eXoVueI18n + + + + diff --git a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml index 13672bd49..365aef8b0 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml @@ -78,6 +78,27 @@ + + PortletEditor + Portlet Editor Portlet + org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet + + portlet-view-dispatched-file-path + /html/portletEditor.html + + -1 + PUBLIC + + text/html + + en + locale.portlet.LayoutEditor + + Portlet Editor + Portlet Editor Management + + + PageTemplatesManagement Page Templates Management Portlet diff --git a/layout-webapp/src/main/webapp/html/portletEditor.html b/layout-webapp/src/main/webapp/html/portletEditor.html new file mode 100644 index 000000000..de9f3a29c --- /dev/null +++ b/layout-webapp/src/main/webapp/html/portletEditor.html @@ -0,0 +1,7 @@ +
+
+ +
+
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 738235fc4..d5cba711f 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 @@ -27,7 +27,7 @@ import './services.js'; // get overridden components if exists if (extensionRegistry) { - const components = extensionRegistry.loadComponents('layoutEditor'); + const components = extensionRegistry.loadComponents('LayoutEditor'); if (components && components.length > 0) { components.forEach(cmp => { Vue.component(cmp.componentName, cmp.componentOptions); diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue new file mode 100644 index 000000000..d5259360a --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue @@ -0,0 +1,38 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue new file mode 100644 index 000000000..29901a551 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue @@ -0,0 +1,27 @@ + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue new file mode 100644 index 000000000..79c9684e9 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue @@ -0,0 +1,45 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue new file mode 100644 index 000000000..9337e9bc4 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js new file mode 100644 index 000000000..556ecd6c5 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js @@ -0,0 +1,36 @@ +/* + * 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 PortletEditor from './components/PortletEditor.vue'; + +import Toolbar from './components/toolbar/Toolbar.vue'; +import SaveButton from './components/toolbar/actions/SaveButton.vue'; + +import Content from './components/content/Content.vue'; + +const components = { + 'portlet-editor': PortletEditor, + 'portlet-editor-toolbar': Toolbar, + 'portlet-editor-toolbar-save-button': SaveButton, + 'portlet-editor-content': Content, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js new file mode 100644 index 000000000..9cca9213e --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js @@ -0,0 +1,66 @@ +/* + * 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 './initComponents.js'; +import '../common-portlets/main.js'; + +// get overridden components if exists +if (extensionRegistry) { + const components = extensionRegistry.loadComponents('PortletEditor'); + if (components && components.length > 0) { + components.forEach(cmp => { + Vue.component(cmp.componentName, cmp.componentOptions); + }); + } +} + +const appId = 'portletEditor'; + +//getting language of the PLF +const lang = eXo?.env.portal.language || 'en'; + +//should expose the locale ressources as REST API +const url = `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.LayoutEditor-${lang}.json`; + +export function init() { + exoi18n.loadLanguageAsync(lang, url) + .then(i18n => { + // init Vue app when locale ressources are ready + Vue.createApp({ + template: ``, + vuetify: Vue.prototype.vuetifyOptions, + data: { + portletInstance: null, + }, + i18n, + created() { + this.$portletInstanceService.getPortletInstance(this.getQueryParam('id')) + .then(data => this.portletInstance = data) + .finally(() => this.$applicationLoaded()); + }, + methods: { + getQueryParam(paramName) { + const uri = window.location.search.substring(1); + const params = new URLSearchParams(uri); + return params.get(paramName); + }, + }, + }, `#${appId}`, 'Portlet Editor'); + }); +} 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 f5d3fa620..434532a6c 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 @@ -19,9 +19,8 @@ --> \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue new file mode 100644 index 000000000..9bd34d811 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue new file mode 100644 index 000000000..c1b50ac18 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue @@ -0,0 +1,57 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue index 29901a551..a9f693499 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue @@ -20,8 +20,8 @@ --> diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue index 9337e9bc4..509c1c0f0 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue @@ -20,13 +20,12 @@ --> @@ -41,22 +40,18 @@ export default { data: () => ({ loading: false, }), - computed: { - canSave() { - return eXo.env.portal.selectedNodeId !== this.$root.nodeId; - }, - }, methods: { - savePage() { - if (!this.canSave) { - return; - } + async save() { this.loading = true; - const layoutToUpdate = this.$layoutUtils.cleanAttributes(this.$root.layout, false, true); - return this.$pageLayoutService.updatePageLayout(this.$root.pageRef, layoutToUpdate, 'contentId', true) - .then(() => this.$root.$emit('layout-page-saved')) - .catch(() => this.$root.$emit('alert-message', this.$t('layout.pageSavingError'), 'error')) - .finally(() => window.setTimeout(() => this.loading = false)); + try { + const instance = await this.$portletInstanceService.getPortletInstance(this.$root.portletInstanceId); + instance.preferences = await this.$portletInstanceService.getPortletInstancePreferences(this.$root.portletInstanceId); + this.$portletInstanceService.updatePortletInstance(instance); + this.$root.$emit('portlet-instance-updated', instance); + this.$root.$emit('alert-message', this.$t('layout.portletInstanceUpdatedSuccessfully'), 'success'); + } finally { + window.setTimeout(() => this.loading = false); + } }, }, }; diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js index 556ecd6c5..f2bc4d6f8 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js @@ -23,12 +23,18 @@ import Toolbar from './components/toolbar/Toolbar.vue'; import SaveButton from './components/toolbar/actions/SaveButton.vue'; import Content from './components/content/Content.vue'; +import Cell from './components/content/Cell.vue'; +import CellResizeButton from './components/content/CellResizeButton.vue'; +import Application from './components/content/Application.vue'; const components = { 'portlet-editor': PortletEditor, 'portlet-editor-toolbar': Toolbar, 'portlet-editor-toolbar-save-button': SaveButton, 'portlet-editor-content': Content, + 'portlet-editor-cell': Cell, + 'portlet-editor-cell-resize-button': CellResizeButton, + 'portlet-editor-application': Application, }; for (const key in components) { diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js index 9cca9213e..db10787ac 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js @@ -46,11 +46,13 @@ export function init() { template: ``, vuetify: Vue.prototype.vuetifyOptions, data: { + portletInstanceId: null, portletInstance: null, }, i18n, created() { - this.$portletInstanceService.getPortletInstance(this.getQueryParam('id')) + this.portletInstanceId = this.getQueryParam('id'); + this.$portletInstanceService.getPortletInstance(this.portletInstanceId) .then(data => this.portletInstance = data) .finally(() => this.$applicationLoaded()); }, 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 85302f68d..16c9c8c47 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/main.js +++ b/layout-webapp/src/main/webapp/vue-app/portlets/main.js @@ -18,8 +18,7 @@ */ import './initComponents.js'; -import '../common/initComponents.js'; -import '../common-illustration/initComponents.js'; +import '../common-illustration/main.js'; import '../common-portlets/main.js'; // get overridden components if exists diff --git a/layout-webapp/src/main/webapp/vue-app/site-management/main.js b/layout-webapp/src/main/webapp/vue-app/site-management/main.js index 5f4ed6512..5dd3f7046 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-management/main.js +++ b/layout-webapp/src/main/webapp/vue-app/site-management/main.js @@ -18,7 +18,6 @@ */ import './initComponents.js'; -import '../common/initComponents.js'; import '../common-layout-components/initComponents.js'; // get overridden components if exists diff --git a/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js b/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js index d861a055f..56e48735d 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js +++ b/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js @@ -18,8 +18,7 @@ */ import './initComponents.js'; -import '../common/initComponents.js'; -import '../common-layout-components/initComponents.js'; +import '../common-layout-components/main.js'; // get overridden components if exists if (extensionRegistry) { From 0b8cf77ecef4b0790c8f0af5527d0ae71f95c45c Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Fri, 31 May 2024 18:16:21 +0100 Subject: [PATCH 3/3] Add unit Tests --- .../service/PortletInstanceRenderService.java | 18 +- .../PortletInstanceImportService.java | 11 +- .../layout/rest/PortletInstanceRestTest.java | 12 +- .../PortletInstanceRenderServiceTest.java | 241 ++++++++++++++++++ 4 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java index d859ea8f5..13052ccd5 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java @@ -21,10 +21,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -86,12 +86,16 @@ public class PortletInstanceRenderService { private Application placeholderApplication; - private Map preferencePlugins = new HashMap<>(); + private Map preferencePlugins = new ConcurrentHashMap<>(); public void addPortletInstancePreferencePlugin(PortletInstancePreferencePlugin plugin) { preferencePlugins.put(plugin.getPortletName(), plugin); } + public void removePortletInstancePreferencePlugin(String portletName) { + preferencePlugins.remove(portletName); + } + public Application getPortletInstanceApplication(String username, // NOSONAR String portletInstanceId, String applicationStorageId) throws IllegalAccessException, @@ -111,9 +115,16 @@ public List getPortletInstancePreferences(long portle String username) throws IllegalAccessException, ObjectNotFoundException { PortletInstance portletInstance = portletInstanceService.getPortletInstance(portletInstanceId, username, null, false); + if (portletInstance == null) { + throw new ObjectNotFoundException(String.format("Portlet Instance with id %s wasn't found", portletInstanceId)); + } PortletInstancePreferencePlugin plugin = preferencePlugins.get(portletInstance.getContentId().split("/")[1]); long applicationId = getPortletInstanceApplicationId(portletInstance.getId()); Application application = layoutService.getApplicationModel(String.valueOf(applicationId)); + if (application == null) { + throw new ObjectNotFoundException(String.format("Application for Portlet Instance with id %s wasn't found", + portletInstanceId)); + } Portlet preferences = layoutService.load(application.getState(), application.getType()); if (plugin == null) { if (preferences == null) { @@ -135,6 +146,9 @@ private Application getOrCreatePortletInstanceApplication(String portletInsta userName, Locale.ENGLISH, false); + if (portletInstance == null) { + throw new ObjectNotFoundException(String.format("Portlet instance with id %s wasn't found", portletInstanceId)); + } long applicationId = getPortletInstanceApplicationId(portletInstance.getId()); if (applicationId == 0) { return createPortletInstanceApplication(portletInstance); 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 index bde42ea52..77cf592dc 100644 --- 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 @@ -201,7 +201,7 @@ protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d LOG.debug("Importing Portlet category instance {}", descriptorId); try { PortletInstanceCategory category = savePortletInstanceCategory(d, oldId); - if (forceReimport || oldId == 0 || category.getId() != oldId) { + if (category != null && (forceReimport || oldId == 0 || category.getId() != oldId)) { LOG.debug("Importing Portlet instance category {} title translations", descriptorId); saveCategoryNames(d, category); // Mark as imported @@ -218,6 +218,9 @@ protected void importPortletInstance(PortletInstanceDescriptor d, long oldId) { LOG.debug("Importing Portlet instance {}", descriptorId); try { PortletInstance portletInstance = savePortletInstance(d, oldId); + if (portletInstance == null) { + return; + } if (forceReimport || oldId == 0 || portletInstance.getId() != oldId) { LOG.debug("Importing Portlet instance {} title translations", descriptorId); saveNames(d, portletInstance); @@ -280,6 +283,12 @@ protected PortletInstanceCategory savePortletInstanceCategory(PortletInstanceCat @SneakyThrows protected PortletInstance savePortletInstance(PortletInstanceDescriptor d, long oldId) { PortletDescriptor portlet = portletService.getPortlet(d.getPortletName()); + if (portlet == null) { + LOG.debug("Saving Portlet instance descriptor {} aborted since portlet {} doesn't exist.", + d.getNameId(), + d.getPortletName()); + return null; + } PortletInstance portletInstance = null; if (oldId > 0) { portletInstance = portletInstanceService.getPortletInstance(oldId); 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 index 0c41b0b84..48de611da 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java @@ -61,6 +61,7 @@ import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.PortletInstanceRenderService; import io.meeds.layout.service.PortletInstanceService; import io.meeds.spring.web.security.PortalAuthenticationManager; import io.meeds.spring.web.security.WebSecurityConfiguration; @@ -94,15 +95,18 @@ public class PortletInstanceRestTest { } @MockBean - private PortletInstanceService portletInstanceService; + private PortletInstanceService portletInstanceService; + + @MockBean + private PortletInstanceRenderService portletInstanceRenderService; @Autowired - private SecurityFilterChain filterChain; + private SecurityFilterChain filterChain; @Autowired - private WebApplicationContext context; + private WebApplicationContext context; - private MockMvc mockMvc; + private MockMvc mockMvc; @BeforeEach void setup() { diff --git a/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java new file mode 100644 index 000000000..69ed463b4 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java @@ -0,0 +1,241 @@ +/** + * 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.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +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.api.settings.SettingService; +import org.exoplatform.commons.api.settings.SettingValue; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.Page; +import org.exoplatform.portal.mop.page.PageContext; +import org.exoplatform.portal.mop.page.PageKey; +import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.pom.spi.portlet.Portlet; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { + PortletInstanceRenderService.class, +}) +@ExtendWith(MockitoExtension.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class PortletInstanceRenderServiceTest { + + private static final String CONTENT_ID = "test/content"; + + private static final String USERNAME = "test"; + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private SettingService settingService; + + @MockBean + private LayoutService layoutService; + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; + + @Mock + private Application application; + + @Mock + private PortletInstance portletInstance; + + @Mock + private Page page; + + @Mock + private Container container; + + @Mock + private PortletInstancePreferencePlugin plugin; + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByInstanceId() { + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(portletInstanceService.getPortletInstance(2, + USERNAME, + Locale.ENGLISH, + false)).thenReturn(portletInstance); + when(layoutService.getApplicationModel("3")).thenReturn(application); + when(portletInstance.getId()).thenReturn(2l); + + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null); + assertEquals(application, portletInstanceApplication); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationPlaceholder() { + assertNotNull(portletInstanceRenderService.getPortletInstanceApplication(null, null, null)); + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWhenNoPlugin() { + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + when(layoutService.getApplicationModel("3")).thenReturn(application); + Portlet portlet = new Portlet(); + when(layoutService.load(any(), any())).thenReturn(portlet); + portlet.setValue("test", "testValue"); + + List portletInstancePreferences = + portletInstanceRenderService.getPortletInstancePreferences(2, + USERNAME); + assertNotNull(portletInstancePreferences); + assertEquals(1, portletInstancePreferences.size()); + assertEquals("test", portletInstancePreferences.get(0).getName()); + assertEquals("testValue", portletInstancePreferences.get(0).getValue()); + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWithPlugin() { + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + when(layoutService.getApplicationModel("3")).thenReturn(application); + when(plugin.getPortletName()).thenReturn("content"); + portletInstanceRenderService.addPortletInstancePreferencePlugin(plugin); + try { + when(plugin.generatePreferences(any(), any())).thenReturn(Collections.singletonList(new PortletInstancePreference("test", + "value"))); + + List portletInstancePreferences = + portletInstanceRenderService.getPortletInstancePreferences(2, + USERNAME); + assertNotNull(portletInstancePreferences); + assertEquals(1, portletInstancePreferences.size()); + assertEquals("test", portletInstancePreferences.get(0).getName()); + assertEquals("value", portletInstancePreferences.get(0).getValue()); + } finally { + portletInstanceRenderService.removePortletInstancePreferencePlugin(plugin.getPortletName()); + } + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWhenNoPluginNoPreferences() { + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(layoutService.getApplicationModel("3")).thenReturn(application); + assertNotNull(portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + assertEquals(0, portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME).size()); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByInstanceIdAndCreateApplication() { + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(portletInstanceService.getPortletInstance(2, + USERNAME, + Locale.ENGLISH, + false)).thenReturn(portletInstance); + + PortletInstancePreference preference = new PortletInstancePreference(); + preference.setName("prefName"); + preference.setValue("prefValue"); + when(portletInstance.getPreferences()).thenReturn(Collections.singletonList(preference)); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + doAnswer(invocation -> { + when(layoutService.getPage(any(PageKey.class))).thenReturn(page); + ArrayList list = new ArrayList<>(); + list.add(container); + when(page.getChildren()).thenReturn(list); + + ArrayList apps = new ArrayList<>(); + apps.add(application); + when(container.getChildren()).thenReturn(apps); + return null; + }).when(layoutService).save(any(PageContext.class), any(Page.class)); + + doAnswer(invocation -> { + ArrayList children = invocation.getArgument(0); + children.set(children.size() - 1, application); + when(container.getChildren()).thenReturn(children); + when(application.getStorageId()).thenReturn("3"); + return null; + }).when(container).setChildren(any()); + + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null); + assertEquals(application, portletInstanceApplication); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByApplicationId() { + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + + when(layoutService.getApplicationModel("3")).thenReturn(application); + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, null, "3"); + assertEquals(application, portletInstanceApplication); + } + +}