From b06c2681cfd960be02835ee3c2dab9e0b51b7706 Mon Sep 17 00:00:00 2001 From: "skory.uladzislau" Date: Wed, 29 Nov 2023 23:15:00 +0100 Subject: [PATCH 01/14] [ERM-3] - Display target path even if page cannot be rolledout there. --- .../core/servlets/CollectLiveCopiesServlet.java | 7 ++++--- .../core/servlets/CollectLiveCopiesServletTest.java | 11 +++++++++-- ...llect-expected-items-with-no-valid-live-copy.json | 12 ++++++++++++ .../core/servlets/collect-expected-items.json | 6 ++++-- .../rollout-manager-ui/js/console-ui.dialog.js | 2 +- 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items-with-no-valid-live-copy.json diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServlet.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServlet.java index 005d6fb..abf84e6 100644 --- a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServlet.java +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServlet.java @@ -75,6 +75,7 @@ public class CollectLiveCopiesServlet extends SlingAllMethodsServlet { private static final String IS_NEW_JSON_FIELD = "isNew"; private static final String HAS_ROLLOUT_TRIGGER_JSON_FIELD = "autoRolloutTrigger"; private static final String LAST_ROLLED_OUT_JSON_FIELD = "lastRolledOut"; + private static final String IS_DISABLED_JSON_FIELD = "disabled"; @Reference private transient LiveRelationshipManager liveRelationshipManager; @@ -137,11 +138,10 @@ private JsonObject relationshipToJson(LiveRelationship relationship, String targetPath = buildTargetPath(relationship, syncPath); LiveCopy liveCopy = relationship.getLiveCopy(); - if (liveCopy == null - || (StringUtils.isNotBlank(syncPath) && !liveCopy.isDeep()) - || !relationshipCheckerService.isAvailableForSync(syncPath, targetPath, liveCopy.getExclusions(), resourceResolver)) { + if (liveCopy == null || (StringUtils.isNotBlank(syncPath) && !liveCopy.isDeep())) { return JsonValue.EMPTY_JSON_OBJECT; } + boolean isDisabled = !relationshipCheckerService.isAvailableForSync(syncPath, targetPath, liveCopy.getExclusions(), resourceResolver); String liveCopyPath = liveCopy.getPath(); boolean isNew = !resourceExists(resourceResolver, liveCopyPath + syncPath); @@ -154,6 +154,7 @@ private JsonObject relationshipToJson(LiveRelationship relationship, .add(IS_NEW_JSON_FIELD, isNew) .add(HAS_ROLLOUT_TRIGGER_JSON_FIELD, !isNew && hasAutoTrigger(liveCopy)) .add(LAST_ROLLED_OUT_JSON_FIELD, getStringDate(resourceResolver, liveCopyPath + syncPath)) + .add(IS_DISABLED_JSON_FIELD, isDisabled) .build(); } diff --git a/core/src/test/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServletTest.java b/core/src/test/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServletTest.java index 631dbbd..078d4d7 100644 --- a/core/src/test/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServletTest.java +++ b/core/src/test/java/com/exadel/etoolbox/rolloutmanager/core/servlets/CollectLiveCopiesServletTest.java @@ -63,6 +63,9 @@ class CollectLiveCopiesServletTest { private static final String EXPECTED_RESPONSE_JSON = "src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items.json"; + private static final String EXPECTED_EMPTY_RESPONSE_JSON = + "src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items-with-no-valid-live-copy.json"; + private final AemContext context = new AemContext(ResourceResolverType.JCR_MOCK); @Mock @@ -142,12 +145,13 @@ void doPost_RelationshipExcludeChildren_EmptyArrayResponse() throws WCMException } @Test - void doPost_NotAvailableForRollout_EmptyArrayResponse() throws WCMException { + void doPost_NotAvailableForRollout_EmptyResponse() throws WCMException, IOException { createSourceResource(); LiveRelationship relationship = mockSingleLiveRelationship(TEST_SOURCE_PATH); LiveCopy liveCopy = mock(LiveCopy.class); + when(liveCopy.getPath()).thenReturn(TEST_LIVE_COPY_PATH); when(relationship.getLiveCopy()).thenReturn(liveCopy); when(relationship.getSyncPath()).thenReturn(TEST_SYNC_PATH); @@ -156,7 +160,10 @@ void doPost_NotAvailableForRollout_EmptyArrayResponse() throws WCMException { .thenReturn(false); fixture.doPost(request, response); - assertEquals(JsonValue.EMPTY_JSON_ARRAY.toString(), response.getOutputAsString()); + + String expected = new String(Files.readAllBytes(Paths.get(EXPECTED_EMPTY_RESPONSE_JSON))) + .replaceAll("(\\r|\\n|\\t|\\s)", StringUtils.EMPTY); + assertEquals(expected, response.getOutputAsString()); } @Test diff --git a/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items-with-no-valid-live-copy.json b/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items-with-no-valid-live-copy.json new file mode 100644 index 0000000..18a15b0 --- /dev/null +++ b/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items-with-no-valid-live-copy.json @@ -0,0 +1,12 @@ +[ + { + "master": "/content/my-site/language-masters/en/testResource", + "path": "/content/my-site/fr/en/testResource", + "depth": 0, + "liveCopies": [], + "isNew": true, + "autoRolloutTrigger": false, + "lastRolledOut": "", + "disabled": true + } +] \ No newline at end of file diff --git a/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items.json b/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items.json index e287e59..7eba55f 100644 --- a/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items.json +++ b/core/src/test/resources/com/exadel/etoolbox/rolloutmanager/core/servlets/collect-expected-items.json @@ -11,11 +11,13 @@ "liveCopies": [], "isNew": true, "autoRolloutTrigger": false, - "lastRolledOut":"" + "lastRolledOut": "", + "disabled": false } ], "isNew": false, "autoRolloutTrigger": false, - "lastRolledOut":"" + "lastRolledOut": "", + "disabled": false } ] \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js index e6d1483..b5f0b05 100644 --- a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js +++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js @@ -181,7 +181,7 @@ data-depth="${liveCopyJson.depth}" data-auto-rollout="${liveCopyJson.autoRolloutTrigger}" value="${liveCopyJson.path}">` - ).text(liveCopyJson.path); + ).text(liveCopyJson.path).attr('disabled', !!liveCopyJson.disabled); const lastRolledOutTimeAgo = $(` Date: Tue, 23 Jul 2024 01:42:01 +0200 Subject: [PATCH 02/14] [ERM-4] - Implement the "Rollout and activate" action --- .../core/models/RolloutItem.java | 24 ++++ .../core/models/RolloutStatus.java | 22 ++++ .../core/services/PageReplicationService.java | 30 +++++ .../impl/PageReplicationServiceImpl.java | 117 ++++++++++++++++++ .../core/servlets/RolloutServlet.java | 63 +++------- .../js/console-ui.actions.js | 3 +- .../js/console-ui.dialog.js | 20 +-- 7 files changed, 226 insertions(+), 53 deletions(-) create mode 100644 core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutItem.java create mode 100644 core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutStatus.java create mode 100644 core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/PageReplicationService.java create mode 100644 core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/impl/PageReplicationServiceImpl.java diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutItem.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutItem.java new file mode 100644 index 0000000..0c0abe1 --- /dev/null +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutItem.java @@ -0,0 +1,24 @@ +package com.exadel.etoolbox.rolloutmanager.core.models; + +public class RolloutItem { + private String master; + private String target; + private int depth; + boolean autoRolloutTrigger; + + public String getMaster() { + return master; + } + + public String getTarget() { + return target; + } + + public int getDepth() { + return depth; + } + + public boolean isAutoRolloutTrigger() { + return autoRolloutTrigger; + } +} diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutStatus.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutStatus.java new file mode 100644 index 0000000..eba32a1 --- /dev/null +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/models/RolloutStatus.java @@ -0,0 +1,22 @@ +package com.exadel.etoolbox.rolloutmanager.core.models; + +public class RolloutStatus { + private boolean isSuccess; + private final String target; + + public RolloutStatus(String target) { + this.target = target; + } + + public boolean isSuccess() { + return isSuccess; + } + + public void setSuccess(boolean success) { + isSuccess = success; + } + + public String getTarget() { + return target; + } +} diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/PageReplicationService.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/PageReplicationService.java new file mode 100644 index 0000000..2c210c5 --- /dev/null +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/PageReplicationService.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exadel.etoolbox.rolloutmanager.core.services; + +import com.day.cq.wcm.api.PageManager; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutItem; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutStatus; +import org.apache.sling.api.resource.ResourceResolver; + +import java.util.List; + +/** + * Provides methods for checking if a live relationship can be synchronized with a blueprint in scope of usage + * the rollout manager tool. + */ +public interface PageReplicationService { + List replicateItems(ResourceResolver resourceResolver, RolloutItem[] items, PageManager pageManager); +} \ No newline at end of file diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/impl/PageReplicationServiceImpl.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/impl/PageReplicationServiceImpl.java new file mode 100644 index 0000000..6c07e80 --- /dev/null +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/services/impl/PageReplicationServiceImpl.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exadel.etoolbox.rolloutmanager.core.services.impl; + +import com.day.cq.replication.ReplicationActionType; +import com.day.cq.replication.ReplicationException; +import com.day.cq.replication.Replicator; +import com.day.cq.wcm.api.Page; +import com.day.cq.wcm.api.PageManager; +import com.day.cq.wcm.msm.api.LiveRelationshipManager; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutItem; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutStatus; +import com.exadel.etoolbox.rolloutmanager.core.services.PageReplicationService; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.event.jobs.JobManager; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Session; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component(service = PageReplicationService.class) +@Designate(ocd = PageReplicationServiceImpl.Configuration.class) +public class PageReplicationServiceImpl implements PageReplicationService { + private static final Logger LOG = LoggerFactory.getLogger(PageReplicationServiceImpl.class); + + @ObjectClassDefinition(name = "EToolbox Page Replication Service Configuration") + @interface Configuration { + + @AttributeDefinition( + name = "Pool size", + description = "The number of Threads in the pool") + int poolSize() default 5; + } + + @Activate + private PageReplicationServiceImpl.Configuration config; + + @Reference + private LiveRelationshipManager liveRelationshipManager; + + @Reference + private JobManager jobManager; + + @Reference + private Replicator replicator; + + public List replicateItems(ResourceResolver resourceResolver, RolloutItem[] items, PageManager pageManager) { + return Arrays.stream(items) + .collect(Collectors.groupingBy(RolloutItem::getDepth)) + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .flatMap(sortedByDepthItems -> replicateSortedByDepthItems(resourceResolver, sortedByDepthItems, pageManager)) + .collect(Collectors.toList()); + } + + private Stream replicateSortedByDepthItems(ResourceResolver resourceResolver, List items, PageManager pageManager) { + ExecutorService executorService = Executors.newFixedThreadPool(config.poolSize()); + return items.stream() + .filter(item -> StringUtils.isNotBlank(item.getTarget())) + .map(item -> CompletableFuture.supplyAsync(() -> replicate(resourceResolver, item, pageManager), executorService)) + .collect(Collectors.toList()) + .stream() + .map(CompletableFuture::join); + } + + private RolloutStatus replicate(ResourceResolver resourceResolver, RolloutItem targetItem, PageManager pageManager) { + + String targetPath = targetItem.getTarget(); + RolloutStatus status = new RolloutStatus(targetPath); + + Optional targetPage = Optional.ofNullable(pageManager.getPage(targetPath)); + Session session = resourceResolver.adaptTo(Session.class); + if (!targetPage.isPresent() || ObjectUtils.isEmpty(session)) { + status.setSuccess(false); + LOG.warn("Replication failed - target page is null, page path: {}", targetPath); + return status; + } + try { + replicator.replicate(session, ReplicationActionType.ACTIVATE, targetPath); + } catch (ReplicationException ex) { + LOG.error("Exception during page replication", ex); + } + status.setSuccess(true); + return status; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/RolloutServlet.java b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/RolloutServlet.java index 547790c..bae4a86 100644 --- a/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/RolloutServlet.java +++ b/core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/servlets/RolloutServlet.java @@ -18,6 +18,9 @@ import com.day.cq.wcm.api.PageManager; import com.day.cq.wcm.api.WCMException; import com.day.cq.wcm.msm.api.RolloutManager; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutItem; +import com.exadel.etoolbox.rolloutmanager.core.models.RolloutStatus; +import com.exadel.etoolbox.rolloutmanager.core.services.PageReplicationService; import com.exadel.etoolbox.rolloutmanager.core.servlets.util.ServletUtil; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.collections.CollectionUtils; @@ -39,6 +42,7 @@ import javax.json.Json; import javax.servlet.Servlet; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -67,11 +71,15 @@ public class RolloutServlet extends SlingAllMethodsServlet { private static final String SELECTION_JSON_ARRAY_PARAM = "selectionJsonArray"; private static final String IS_DEEP_ROLLOUT_PARAM = "isDeepRollout"; + private static final String SHOULD_ACTIVATE_PARAM = "shouldActivate"; private static final String FAILED_TARGETS_RESPONSE_PARAM = "failedTargets"; @Reference private transient RolloutManager rolloutManager; + @Reference + private transient PageReplicationService pageReplicationService; + @Override protected void doPost(final SlingHttpServletRequest request, final SlingHttpServletResponse response) { StopWatch sw = StopWatch.createStarted(); @@ -103,7 +111,16 @@ protected void doPost(final SlingHttpServletRequest request, final SlingHttpServ LOG.debug("Is deep rollout (include subpages): {}", isDeepRollout); List rolloutStatuses = doItemsRollout(rolloutItems, pageManager, isDeepRollout); - writeStatusesIfFailed(rolloutStatuses, response); + + boolean shouldActivate = ServletUtil.getRequestParamBoolean(request, SHOULD_ACTIVATE_PARAM); + LOG.debug("Should activate pages: {}", shouldActivate); + List activationStatuses = new ArrayList<>(); + if (shouldActivate) { + activationStatuses = pageReplicationService.replicateItems(request.getResourceResolver(), rolloutItems, request.getResourceResolver().adaptTo(PageManager.class)); + } + + writeStatusesIfFailed(Stream.concat(rolloutStatuses.stream(), activationStatuses.stream()) + .collect(Collectors.toList()), response); LOG.debug("Rollout of selected items is completed in {} ms", sw.getTime(TimeUnit.MILLISECONDS)); } @@ -192,48 +209,4 @@ private RolloutItem[] jsonArrayToRolloutItems(String jsonArray) { } return new RolloutItem[0]; } - - private static class RolloutItem { - private String master; - private String target; - private int depth; - boolean autoRolloutTrigger; - - public String getMaster() { - return master; - } - - public String getTarget() { - return target; - } - - public int getDepth() { - return depth; - } - - public boolean isAutoRolloutTrigger() { - return autoRolloutTrigger; - } - } - - private static class RolloutStatus { - private boolean isSuccess; - private final String target; - - public RolloutStatus(String target) { - this.target = target; - } - - public boolean isSuccess() { - return isSuccess; - } - - public void setSuccess(boolean success) { - isSuccess = success; - } - - public String getTarget() { - return target; - } - } } \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.actions.js b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.actions.js index b9a421e..c5846f2 100644 --- a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.actions.js +++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.actions.js @@ -108,7 +108,8 @@ data: { _charset_: 'UTF-8', selectionJsonArray: JSON.stringify(data.selectionJsonArray), - isDeepRollout: data.isDeepRollout + isDeepRollout: data.isDeepRollout, + shouldActivate: data.shouldActivate } }).fail((xhr) => { logger.log(getProcessingErrorMsg(xhr), false); diff --git a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js index b5f0b05..ce88587 100644 --- a/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js +++ b/ui.apps/src/main/content/jcr_root/apps/etoolbox-rollout-manager/clientlibs/rollout-manager-ui/js/console-ui.dialog.js @@ -108,6 +108,7 @@ // Rollout dialog related constants const CANCEL_LABEL = Granite.I18n.get('Cancel'); const DIALOG_LABEL = Granite.I18n.get('Rollout'); + const ROLLOUT_AND_PUBLISH_LABEL = Granite.I18n.get('Rollout and Publish'); const SELECT_ALL_LABEL = Granite.I18n.get('Select all'); const UNSELECT_ALL_LABEL = Granite.I18n.get('Unselect all'); const TARGET_PATHS_LABEL = Granite.I18n.get('Target paths'); @@ -246,7 +247,8 @@ onCheckboxChange(submitBtn); } - function onResolve(path, deferred) { + function onResolve($btn, path, deferred) { + const shouldActivate = $btn.closest('[data-dialog-action]').data('dialogAction') === 'rolloutPublish'; const isDeepRollout = $('coral-checkbox[name="isDeepRollout"]').prop('checked'); const selectionJsonArray = []; $(CORAL_CHECKBOX_ITEM).each(function () { @@ -258,7 +260,8 @@ const data = { path, isDeepRollout, - selectionJsonArray + selectionJsonArray, + shouldActivate }; deferred.resolve(data); } @@ -285,8 +288,11 @@ const deferred = $.Deferred(); const dialog = initRolloutDialog(selectedPath); - const $submitBtn = $('