From ad19b68ef8b1e7c3905a8812a695ff3fa881c1f5 Mon Sep 17 00:00:00 2001 From: Sofien Haj Chedhli Date: Tue, 29 Oct 2024 10:15:32 +0100 Subject: [PATCH] feat: Implement an upgrade plugin for content articles attachments migration and adapt the NewsArticleUpgradePlugin - EXO-74446 --- data-upgrade-news/pom.xml | 5 + .../ArticleAttachmentsUpgradePlugin.java | 170 ++++++++++++++++++ .../news/upgrade/jcr/NewsArticlesUpgrade.java | 30 ++-- .../resources/conf/portal/configuration.xml | 33 ++++ .../ArticleAttachmentsUpgradePluginTest.java | 121 +++++++++++++ .../upgrade/jcr/NewsArticlesUpgradeTest.java | 9 +- 6 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePlugin.java create mode 100644 data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePluginTest.java diff --git a/data-upgrade-news/pom.xml b/data-upgrade-news/pom.xml index e4428f389..d3a73ce8c 100644 --- a/data-upgrade-news/pom.xml +++ b/data-upgrade-news/pom.xml @@ -43,6 +43,11 @@ ecms-ext-authoring-services provided + + org.exoplatform.ecms + ecms-core-services + provided + io.meeds.content content-service diff --git a/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePlugin.java b/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePlugin.java new file mode 100644 index 000000000..41d23f465 --- /dev/null +++ b/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePlugin.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 eXo Platform SAS. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.exoplatform.news.upgrade; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import org.apache.commons.lang3.StringUtils; +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.persistence.impl.EntityManagerService; +import org.exoplatform.commons.upgrade.UpgradeProductPlugin; +import org.exoplatform.container.ExoContainerContext; +import org.exoplatform.container.PortalContainer; +import org.exoplatform.container.component.RequestLifeCycle; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.services.attachments.storage.AttachmentStorage; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class ArticleAttachmentsUpgradePlugin extends UpgradeProductPlugin { + + private static final Log LOG = ExoLogger.getLogger(ArticleAttachmentsUpgradePlugin.class); + + private SettingService settingService; + + private NoteService noteService; + + private EntityManagerService entityManagerService; + + private AttachmentStorage attachmentStorage; + + private final PortalContainer container; + + private static final String ARTICLES_UPGRADE_EXECUTED_KEY = "articlesUpgradeExecuted"; + + private static final String ARTICLES_UPGRADE_PLUGIN_NAME = "NewsArticlesUpgradePlugin"; + + public ArticleAttachmentsUpgradePlugin(InitParams initParams, + EntityManagerService entityManagerService, + SettingService settingService, + NoteService noteService, + AttachmentStorage attachmentStorage, PortalContainer container) { + super(initParams); + this.settingService = settingService; + this.entityManagerService = entityManagerService; + this.noteService = noteService; + this.attachmentStorage = attachmentStorage; + this.container = container; + } + + @Override + public boolean shouldProceedToUpgrade(String newVersion, String previousGroupVersion) { + SettingValue settingValue = settingService.get(Context.GLOBAL.id(ARTICLES_UPGRADE_PLUGIN_NAME), + Scope.APPLICATION.id(ARTICLES_UPGRADE_PLUGIN_NAME), + ARTICLES_UPGRADE_EXECUTED_KEY); + if (settingValue == null || settingValue.getValue().equals("false")) { + return false; + } + return super.shouldProceedToUpgrade(newVersion, previousGroupVersion); + } + + @Override + public void processUpgrade(String oldVersion, String newVersion) { + ExoContainerContext.setCurrentContainer(container); + long startupTime = System.currentTimeMillis(); + int attachmentCount = 0; + int articleCount = 0; + boolean transactionStarted = false; + RequestLifeCycle.begin(this.entityManagerService); + EntityManager entityManager = this.entityManagerService.getEntityManager(); + try { + if (!entityManager.getTransaction().isActive()) { + entityManager.getTransaction().begin(); + transactionStarted = true; + } + + List results = getAttachments(entityManager); + if (results.isEmpty()) { + return; + } + LOG.info("Start updating article attachments, {} articles should be updated", results.size()); + for (Object[] result : results) { + String versionId = (String) result[0]; + String originalVersionId = versionId; + String propertyValue = (String) result[1]; // get the value + PageVersion pageVersion = noteService.getPageVersionById(Long.valueOf(versionId)); + if (pageVersion != null && pageVersion.getParent() != null && StringUtils.isNotEmpty(pageVersion.getParent().getId())) { + PageVersion latestVersion = + noteService.getPublishedVersionByPageIdAndLang(Long.valueOf(pageVersion.getParent().getId()), + null); + versionId = latestVersion.getId(); + } + String[] attachmentIds = propertyValue.split(";"); + attachmentCount += linkAttachmentsToEntity(versionId, attachmentIds); + articleCount += 1; + LOG.info("{} attachments linked to {} articles", attachmentCount, articleCount); + + removeAttachmentProperty(originalVersionId, entityManager, transactionStarted); + } + if (transactionStarted && entityManager.getTransaction().isActive()) { + entityManager.getTransaction().commit(); + } + LOG.info("Updating article attachments done it took {} ms", System.currentTimeMillis() - startupTime); + } catch (Exception e) { + if (transactionStarted && entityManager.getTransaction().isActive() && entityManager.getTransaction().getRollbackOnly()) { + entityManager.getTransaction().rollback(); + } + LOG.error("Error when processing article attachments upgrade plugin", e); + } finally { + RequestLifeCycle.end(); + } + } + + private int linkAttachmentsToEntity(String articleId, String[] attachmentIds) { + AtomicInteger linkedAttachments = new AtomicInteger(0); + + Arrays.stream(attachmentIds).forEach(id -> { + try { + // Link the attachment to the entity + this.attachmentStorage.linkAttachmentToEntity(Long.parseLong(articleId), "WIKI_PAGE_VERSIONS", id); + linkedAttachments.incrementAndGet(); + } catch (Exception e) { + LOG.error("Error when linking attachment with id {} to entity with id {}", id, articleId, e); + } + }); + + return linkedAttachments.get(); + } + + private void removeAttachmentProperty(String articleId, EntityManager entityManager, boolean transactionStarted) { + + String deleteQuery = "DELETE FROM SOC_METADATA_ITEMS_PROPERTIES " + + "WHERE metadata_item_id IN (SELECT metadata_item_id FROM SOC_METADATA_ITEMS WHERE object_id = '" + articleId + "')" + + " AND name = 'attachmentsIds'"; + + Query query = entityManager.createNativeQuery(deleteQuery); + query.executeUpdate(); + } + + private List getAttachments(EntityManager entityManager) { + String selectQuery = "SELECT mi.object_id, p.value " + "FROM SOC_METADATA_ITEMS mi " + "JOIN SOC_METADATA_ITEMS_PROPERTIES p " + + "ON mi.metadata_item_id = p.metadata_item_id " + "WHERE mi.object_type = 'newsPageVersion' " + + "AND p.name = 'attachmentsIds' " + "AND mi.object_id IS NOT NULL " + "AND mi.object_id != '' " + + "AND p.value IS NOT NULL " + "AND p.value != '';"; + Query nativeQuery = entityManager.createNativeQuery(selectQuery); + return nativeQuery.getResultList(); + } +} diff --git a/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgrade.java b/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgrade.java index f3816d5b4..5606498d1 100644 --- a/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgrade.java +++ b/data-upgrade-news/src/main/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgrade.java @@ -40,6 +40,7 @@ import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.exoplatform.commons.api.settings.SettingService; import org.exoplatform.commons.api.settings.SettingValue; import org.exoplatform.commons.api.settings.data.Context; @@ -52,6 +53,7 @@ import org.exoplatform.commons.utils.CommonsUtils; import org.exoplatform.commons.utils.HTMLSanitizer; import org.exoplatform.container.xml.InitParams; +import org.exoplatform.services.attachments.storage.AttachmentStorage; import org.exoplatform.services.jcr.RepositoryService; import org.exoplatform.services.jcr.ext.app.SessionProviderService; import org.exoplatform.services.jcr.ext.common.SessionProvider; @@ -110,6 +112,8 @@ public class NewsArticlesUpgrade extends UpgradeProductPlugin { private SettingService settingService; + private AttachmentStorage attachmentStorage; + private int migratedNewsArticlesCount = 0; public static final MetadataType NEWS_METADATA_TYPE = new MetadataType(1000, "news"); @@ -138,6 +142,7 @@ public NewsArticlesUpgrade(InitParams initParams, NoteService noteService, IdentityManager identityManager, IndexingService indexingService, + AttachmentStorage attachmentStorage, SettingService settingService) { super(initParams); this.repositoryService = repositoryService; @@ -151,6 +156,7 @@ public NewsArticlesUpgrade(InitParams initParams, this.identityManager = identityManager; this.indexingService = indexingService; this.settingService = settingService; + this.attachmentStorage = attachmentStorage; } @Override @@ -279,7 +285,7 @@ public int manageNewsArticles(List newsArticlesNodes, Session session) thr } PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.parseLong(article.getId()), null); setArticleIllustration(pageVersion.getParent(), article.getSpaceId(), newsArticleNode, "notePage"); - setArticleAttachments(pageVersion.getId(), article.getSpaceId(), newsArticleNode, "newsPageVersion"); + setArticleAttachments(pageVersion.getId(), newsArticleNode); /* upgrade news id for news targets and favorite metadatata items */ setArticleMetadatasItems(article.getId(), newsArticleNode.getUUID()); if (getStringProperty(newsArticleNode, "publication:currentState").equals("published")) { @@ -330,7 +336,7 @@ public int manageNewsArticles(List newsArticlesNodes, Session session) thr } PageVersion pageVersion = noteService.getPublishedVersionByPageIdAndLang(Long.parseLong(article.getId()), null); setArticleIllustration(pageVersion.getParent(), article.getSpaceId(), publishedNode, "notePage"); - setArticleAttachments(pageVersion.getId(), article.getSpaceId(), publishedNode, "newsPageVersion"); + setArticleAttachments(pageVersion.getId(), publishedNode); /* upgrade news id for news targets and favorite metadatata items */ setArticleMetadatasItems(article.getId(), newsArticleNode.getUUID()); setArticleActivities(article, publishedNode); @@ -544,28 +550,14 @@ private void setArticleViews(News article, Node newsNode) throws RepositoryExcep } private void setArticleAttachments(String articleId, - String spaceId, - Node newsNode, - String articleObjectType) throws RepositoryException { + Node newsNode) throws RepositoryException { if (newsNode.hasProperty("exo:attachmentsIds")) { Property attachmentsIdsProperty = newsNode.getProperty("exo:attachmentsIds"); - String attachmentsIds = ""; for (Value value : attachmentsIdsProperty.getValues()) { String attachmentId = value.getString(); - attachmentsIds += attachmentId + ";"; - } - MetadataObject articleMetaDataObject = new MetadataObject(articleObjectType, articleId, null, Long.parseLong(spaceId)); - MetadataItem articleMetadataItem = metadataService.getMetadataItemsByMetadataAndObject(NEWS_METADATA_KEY, - articleMetaDataObject) - .get(0); - if (articleMetadataItem != null) { - Map articleMetadataItemProperties = articleMetadataItem.getProperties(); - if (articleMetadataItemProperties == null) { - articleMetadataItemProperties = new HashMap<>(); + if (StringUtils.isNotEmpty(attachmentId) && StringUtils.isNotEmpty(articleId)) { + attachmentStorage.linkAttachmentToEntity(Long.valueOf(articleId), "WIKI_PAGE_VERSIONS", attachmentId); } - articleMetadataItemProperties.put("attachmentsIds", attachmentsIds); - articleMetadataItem.setProperties(articleMetadataItemProperties); - metadataService.updateMetadataItem(articleMetadataItem, articleMetadataItem.getCreatorId()); } } } diff --git a/data-upgrade-news/src/main/resources/conf/portal/configuration.xml b/data-upgrade-news/src/main/resources/conf/portal/configuration.xml index 53de8dd9e..a59c06680 100644 --- a/data-upgrade-news/src/main/resources/conf/portal/configuration.xml +++ b/data-upgrade-news/src/main/resources/conf/portal/configuration.xml @@ -81,6 +81,39 @@ + + ContentArticleAttachmentsUpgrade + addUpgradePlugin + org.exoplatform.news.upgrade.ArticleAttachmentsUpgradePlugin + Migrate content attachments from metadata properties to attachments context + + + product.group.id + The groupId of the product + org.exoplatform.platform + + + plugin.execution.order + The plugin execution order + 1 + + + plugin.upgrade.execute.once + Execute this upgrade plugin only once + true + + + plugin.upgrade.async.execution + The plugin will be executed in an asynchronous mode + true + + + plugin.upgrade.target.version + Target version of the plugin + 7.0.0 + + + diff --git a/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePluginTest.java b/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePluginTest.java new file mode 100644 index 000000000..3689f3043 --- /dev/null +++ b/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/ArticleAttachmentsUpgradePluginTest.java @@ -0,0 +1,121 @@ +package org.exoplatform.news.upgrade; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.Query; +import org.exoplatform.commons.api.settings.SettingService; +import org.exoplatform.commons.api.settings.SettingValue; +import org.exoplatform.commons.persistence.impl.EntityManagerService; +import org.exoplatform.container.PortalContainer; +import org.exoplatform.container.xml.InitParams; +import org.exoplatform.container.xml.ValueParam; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; +import org.exoplatform.services.attachments.storage.AttachmentStorage; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ArticleAttachmentsUpgradePluginTest { + + @Mock + private SettingService settingService; + + @Mock + private NoteService noteService; + + @Mock + private EntityManagerService entityManagerService; + + @Mock + private AttachmentStorage attachmentStorage; + + @Mock + private EntityManager entityManager; + + @Mock + private EntityTransaction transaction; + + @Mock + PortalContainer container; + + private ArticleAttachmentsUpgradePlugin articleAttachmentsUpgradePlugin; + + private static final String ARTICLES_UPGRADE_EXECUTED_KEY = "articlesUpgradeExecuted"; + + @Before + public void setUp() { + InitParams initParams = new InitParams(); + ValueParam valueParam = new ValueParam(); + valueParam.setName("product.group.id"); + valueParam.setValue("org.exoplatform.platform"); + initParams.addParameter(valueParam); + articleAttachmentsUpgradePlugin = new ArticleAttachmentsUpgradePlugin(initParams, + entityManagerService, + settingService, + noteService, + attachmentStorage,container); + + when(entityManagerService.getEntityManager()).thenReturn(entityManager); + when(entityManager.getTransaction()).thenReturn(transaction); + } + + @Test + public void shouldNotProceedToUpgrade_WhenSettingIsFalse() { + + SettingValue settingValue = mock(SettingValue.class); + when(settingValue.getValue()).thenReturn("false"); + when(settingService.get(any(), any(), anyString())).thenReturn(settingValue, null); + + boolean result = articleAttachmentsUpgradePlugin.shouldProceedToUpgrade("6.5.4", "7.0.0"); + + // Validate the result + assertFalse(result); + verify(settingService, times(1)).get(any(), any(), eq(ARTICLES_UPGRADE_EXECUTED_KEY)); + } + + @Test + public void processUpgrade_ShouldLinkAttachmentsAndCommitTransaction() { + + when(entityManager.getTransaction().isActive()).thenReturn(false).thenReturn(true); + doNothing().when(transaction).begin(); + doNothing().when(transaction).commit(); + + // Mock getAttachments query results + List mockResults = Collections.singletonList(new Object[]{"123", "attachment1;attachment2"}); + Query selectQuery = mock(Query.class); + Query deleteQuery = mock(Query.class); + when(entityManager.createNativeQuery(any())).thenReturn(selectQuery).thenReturn(deleteQuery); + when(selectQuery.getResultList()).thenReturn(mockResults); + + PageVersion mockPageVersion = mock(PageVersion.class); + when(noteService.getPageVersionById(anyLong())).thenReturn(mockPageVersion); + when(mockPageVersion.getParent()).thenReturn(null); // No parent to simplify this test + + // Run processUpgrade + articleAttachmentsUpgradePlugin.processUpgrade("6.5.4", "7.0.0"); + + // Verify behaviors + verify(transaction, times(1)).begin(); + verify(attachmentStorage, times(2)).linkAttachmentToEntity(any(Long.class), any(String.class), any(String.class)); + verify(transaction, times(1)).commit(); + } + +} diff --git a/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgradeTest.java b/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgradeTest.java index d9c489cf1..502c3743f 100644 --- a/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgradeTest.java +++ b/data-upgrade-news/src/test/java/org/exoplatform/news/upgrade/jcr/NewsArticlesUpgradeTest.java @@ -47,6 +47,7 @@ import org.exoplatform.commons.api.settings.SettingService; import org.exoplatform.commons.api.settings.SettingValue; import org.exoplatform.commons.upgrade.UpgradePluginExecutionContext; +import org.exoplatform.services.attachments.storage.AttachmentStorage; import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.manager.IdentityManager; import org.junit.AfterClass; @@ -129,6 +130,9 @@ public class NewsArticlesUpgradeTest { @Mock private SettingService settingService; + @Mock + private AttachmentStorage attachmentStorage; + private NewsArticlesUpgrade newsArticlesUpgrade; @AfterClass @@ -156,6 +160,7 @@ public void setUp() { noteService, identityManager, indexingService, + attachmentStorage, settingService); } @@ -296,8 +301,8 @@ public void testProcessUpgrade() throws Exception { verify(newsService, times(2)).createNewsArticlePage(any(News.class), anyString()); verify(noteService, times(2)).getPublishedVersionByPageIdAndLang(anyLong(), nullable(String.class)); - verify(metadataService, times(7)).getMetadataItemsByMetadataAndObject(any(), any(MetadataObject.class)); - verify(metadataService, times(7)).updateMetadataItem(any(), anyLong()); + verify(metadataService, times(6)).getMetadataItemsByMetadataAndObject(any(), any(MetadataObject.class)); + verify(metadataService, times(6)).updateMetadataItem(any(), anyLong()); verify(activityManager, times(1)).getActivity(any()); verify(activityManager, times(1)).updateActivity(any(ExoSocialActivity.class), eq(false)); }