com.adobe.acs.commons.mcp.impl.processes.renovator.Renovator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of acs-aem-commons-bundle Show documentation
Show all versions of acs-aem-commons-bundle Show documentation
Core ACS AEM Commons OSGi Bundle. Includes commons utilities.
/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2017 Adobe
* %%
* 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.
* #L%
*/
package com.adobe.acs.commons.mcp.impl.processes.renovator;
import com.adobe.acs.commons.data.Spreadsheet;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.fam.actions.Actions;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.CheckboxComponent;
import com.adobe.acs.commons.mcp.form.Description;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.form.PathfieldComponent.NodeSelectComponent;
import com.adobe.acs.commons.mcp.form.RadioComponent;
import com.adobe.acs.commons.mcp.form.TextfieldComponent;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.adobe.acs.commons.mcp.model.ManagedProcess;
import com.adobe.acs.commons.util.visitors.TraversalException;
import com.adobe.acs.commons.util.visitors.TreeFilteringResourceVisitor;
import com.day.cq.audit.AuditLog;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.Replicator;
import com.day.cq.tagging.TagConstants;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.PageManagerFactory;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.adobe.acs.commons.mcp.impl.processes.renovator.Util.*;
import static com.day.cq.commons.jcr.JcrConstants.JCR_PRIMARYTYPE;
/**
* Relocate Pages and/or Sites using a parallelized move process
*/
public class Renovator extends ProcessDefinition {
private static final String DESTINATION_COL = "destination";
private static final String SOURCE_COL = "source";
private static final transient Logger LOG = LoggerFactory.getLogger(Renovator.class);
public Renovator(PageManagerFactory pageManagerFactory, Replicator replicator, AuditLog auditLog) {
this.pageManagerFactory = pageManagerFactory;
this.replicator = replicator;
this.auditLog = auditLog;
}
private final PageManagerFactory pageManagerFactory;
private final Replicator replicator;
private final AuditLog auditLog;
public enum PublishMethod {
@Description("No publishing will occur")
NONE,
@Description("Publishing will be managed by MCP and the queue is left unaffected so regular publishing can still occur")
SELF_MANAGED,
@Description("Publishing is handled by the product publish queue, not recommended very large jobs")
QUEUE
}
@FormField(name = "Multiple moves",
description = "Excel spreadsheet for performing multiple moves",
component = FileUploadComponent.class,
required = false)
private RequestParameter sourceFile;
@FormField(name = "Source",
description = "Select page/site to be moved for single move",
hint = "/content/my-site/en/my-page",
component = NodeSelectComponent.class,
required = false,
options = {"base=/content"})
private String sourceJcrPath;
@FormField(name = "Destination",
description = "Destination location (must include new name for source node even if same)",
hint = "Move: /content/new-place/my-page -OR- Rename: /content/new-place/new-name",
component = NodeSelectComponent.class,
required = false,
options = {"base=/content"})
private String destinationJcrPath;
@FormField(name = "Max References",
description = "Limit of how many page references to handle (max per page)",
hint = "-1 = All, 0 = None, etc.",
component = TextfieldComponent.class,
required = false,
options = {"default=-1"})
private int maxReferences = -1;
/*
@FormField(name = "Reference Search Root",
description = "Root for reference searches. Depending on how indexes are set up, / might be the only working value on your system",
hint = "/ (all), /content, ...",
component = TextfieldComponent.class,
required = false,
options = {"default=/"})
*/
private String referenceSearchRoot = "/";
@FormField(name = "Publish",
description = "Self-managed handles publishing in-process where as Queue will add it to the system publish queue where progress is not tracked here.",
component = RadioComponent.EnumerationSelector.class,
options = {"vertical", "default=SELF_MANAGED"})
public PublishMethod publishMethod = PublishMethod.SELF_MANAGED;
@FormField(name = "Create versions",
description = "Create versions for anything being updated/replicated",
component = CheckboxComponent.class,
options = {"checked"})
private boolean createVerionsOnReplicate;
@FormField(name = "Update status",
description = "Updates status of content affected by this operation",
component = CheckboxComponent.class,
options = {"checked"})
private boolean updateStatus;
@FormField(name = "Extensive ACL checks",
description = "If checked, this evaluates ALL nodes. If not checked, it only evaluates pages.",
component = CheckboxComponent.class)
private boolean extensiveACLChecks = false;
@FormField(name = "Dry run",
description = "This runs the ACL checks but doesn't do any actual work.",
component = CheckboxComponent.class,
options = {"checked"})
private boolean dryRun = true;
@FormField(name = "Audit Trails",
description = "This will update audit trail entries based on what is moved.",
component = CheckboxComponent.class)
private boolean auditTrails = false;
@FormField(name = "Detailed report",
description = "Record extra details in the report, can be rather extensive. Not recommended for large jobs.",
component = CheckboxComponent.class)
private boolean detailedReport = false;
private final transient String[] requiredMovePrivilegeNames = {
Privilege.JCR_READ,
Privilege.JCR_WRITE,
Privilege.JCR_REMOVE_CHILD_NODES,
Privilege.JCR_REMOVE_NODE,
Replicator.REPLICATE_PRIVILEGE
};
Privilege[] requiredMovePrivileges;
private final transient String[] requiredPublishPrivilegeNames = {
Privilege.JCR_READ,
Privilege.JCR_WRITE,
Replicator.REPLICATE_PRIVILEGE
};
Privilege[] requiredPublishPrivileges;
private final transient String[] requiredUpdatePrivilegeNames = {
Privilege.JCR_READ,
Privilege.JCR_WRITE
};
Privilege[] requiredUpdatePrivileges;
private final transient String[] requiredAuditPrivilegeNames = {
Privilege.JCR_READ,
PrivilegeConstants.REP_WRITE
};
Privilege[] requiredAuditPrivileges;
ReplicatorQueue replicatorQueue = new ReplicatorQueue();
ReplicationOptions replicationOptions;
private final Set moves = Collections.synchronizedSet(new HashSet<>());
private final Set additionalTargetFolders = Collections.synchronizedSet(new TreeSet<>());
final Map movePaths = Collections.synchronizedMap(new HashMap<>());
@Override
public void init() throws RepositoryException {
replicationOptions = new ReplicationOptions();
switch (publishMethod) {
case SELF_MANAGED:
replicationOptions.setSynchronous(true);
break;
default:
replicationOptions.setSynchronous(false);
break;
}
replicationOptions.setSuppressVersions(!createVerionsOnReplicate);
replicationOptions.setSuppressStatusUpdate(!updateStatus);
if (referenceSearchRoot == null || referenceSearchRoot.trim().isEmpty()) {
referenceSearchRoot = "/";
}
}
private void validateInputs(ResourceResolver res) throws RepositoryException {
if (sourceFile != null && sourceFile.getSize() > 0) {
validateSpreadsheetInput();
} else {
validateSingleMoveInput();
}
for (Map.Entry entry : movePaths.entrySet()) {
String sourcePath = entry.getKey();
String destinationPath = entry.getValue();
validateMovePreconditions(res, sourcePath, destinationPath);
}
}
private void validateMovePreconditions(ResourceResolver res, String sourcePath, String destinationPath) throws RepositoryException {
if (destinationPath.contains(sourcePath + "/")) {
throw new RepositoryException("Destination must be outside of source path");
}
if (!resourceExists(res, sourcePath)) {
if (!sourcePath.startsWith("/")) {
throw new RepositoryException("Paths are not valid unless they start with a forward slash, you provided: " + sourcePath);
} else {
throw new RepositoryException("Unable to find source " + sourcePath);
}
}
if (!resourceExists(res, destinationPath.substring(0, destinationPath.lastIndexOf('/')))) {
if (!destinationPath.startsWith("/")) {
throw new RepositoryException("Paths are not valid unless they start with a forward slash, you provided: " + destinationPath);
} else if (!destinationPath.startsWith(DAM_ROOT)) {
throw new RepositoryException("Unable to find destination " + destinationPath);
}
}
if (sourcePath.startsWith(DAM_ROOT) != destinationPath.startsWith(DAM_ROOT)) {
throw new RepositoryException("Source and destination are incompatible (if one is in the DAM, then so should the other be in the DAM)");
}
}
private void validateSingleMoveInput() throws RepositoryException {
if (sourceJcrPath == null) {
throw new RepositoryException("Source path should not be null if no file provided");
}
if (destinationJcrPath == null) {
throw new RepositoryException("Destination path should not be null if no file provided");
}
movePaths.put(sourceJcrPath, destinationJcrPath);
}
private void validateSpreadsheetInput() throws RepositoryException {
Spreadsheet sheet;
try {
sheet = new Spreadsheet(sourceFile, SOURCE_COL, DESTINATION_COL).buildSpreadsheet();
} catch (IOException ex) {
throw new RepositoryException("Unable to parse spreadsheet", ex);
}
if (!sheet.getHeaderRow().contains(SOURCE_COL) || !sheet.getHeaderRow().contains(DESTINATION_COL)) {
throw new RepositoryException(MessageFormat.format("Spreadsheet should have two columns, respectively named {0} and {1}", SOURCE_COL, DESTINATION_COL));
}
sheet.getDataRowsAsCompositeVariants().forEach(row -> {
movePaths.put(row.get(SOURCE_COL).toString(),
row.get(DESTINATION_COL).toString());
});
}
private static final String DAM_ROOT = "/content/dam";
private static final String AUDIT_ROOT = "/var/audit";
ManagedProcess instanceInfo;
@Override
public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException, RepositoryException {
validateInputs(rr);
instanceInfo = instance.getInfo();
String desc = dryRun ? "DRY RUN: " : "";
desc += "Publish mode " + publishMethod.name().toLowerCase();
instance.getInfo().setDescription(desc);
requiredMovePrivileges = getPrivilegesFromNames(rr, requiredMovePrivilegeNames);
requiredUpdatePrivileges = getPrivilegesFromNames(rr, requiredUpdatePrivilegeNames);
requiredPublishPrivileges = getPrivilegesFromNames(rr, requiredPublishPrivilegeNames);
requiredAuditPrivileges = getPrivilegesFromNames(rr, requiredAuditPrivilegeNames);
instance.defineCriticalAction("Eval Struct", rr, this::identifyStructure);
instance.defineCriticalAction("Eval Refs", rr, this::identifyReferences);
instance.defineCriticalAction("Check ACLs", rr, this::validateAllAcls);
if (!dryRun) {
instance.defineCriticalAction("Build destination", rr, this::buildStructures);
instance.defineCriticalAction("Move Tree", rr, this::moveTree);
if(auditTrails) {
instance.defineAction("Add Move Audit Entries", rr, this::addMoveAuditEntries);
instance.defineAction("Create Audit Structure Folders", rr, this::buildAuditStructure);
instance.defineAction("Move Audit Entries", rr, this::moveAudits);
}
if (publishMethod != PublishMethod.NONE) {
instance.defineAction("Activate Tree", rr, this::activateTreeStructure);
instance.defineAction("Activate New", rr, this::activateNew);
instance.defineAction("Activate References", rr, this::activateReferences);
instance.defineAction("Deactivate Old", rr, this::deactivateOld);
}
instance.defineAction("Remove source", rr, this::removeSource);
}
}
private void addMoveAuditEntries(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
node.visit(childNode -> {
LOG.debug("adding audit entry for move of {} to {}", childNode.getSourcePath(), childNode.getDestinationPath());
childNode.addAuditRecordForMove(rr, auditLog);
});
});
});
}
private void buildAuditStructure(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
node.visit(childNode -> {
if(childNode.isAuditableMove()) {
manager.deferredWithResolver(rr2 -> {
String[] categories = auditLog.getCategories();
buildAuditCategoryFolders(rr2, childNode, categories);
});
}
});
});
});
}
private void buildAuditCategoryFolders(ResourceResolver rr2, MovingNode childNode, String[] categories) throws PersistenceException {
for (String auditCategory : categories) {
Resource sourceAuditRes = getAuditCategoryResource(rr2, auditCategory, childNode.getSourcePath());
if (sourceAuditRes != null) {
LOG.debug("Found audit source at {}", sourceAuditRes.getPath());
getOrCreateAuditCategoryResource(rr2, auditCategory, childNode.getDestinationPath());
}
}
}
private Resource getOrCreateAuditCategoryResource(ResourceResolver rr, String auditCategory, String contentPath) throws PersistenceException {
Resource auditCategoryRes = getAuditCategoryResource(rr, auditCategory, contentPath);
if (auditCategoryRes == null) {
String auditCategoryPath = AUDIT_ROOT + "/" + auditCategory.replace('/', '.');
//this should, at least in theory always exist, because by the time we get here we know an entry exists for the source exists
//but null check JIC
Resource auditCatRootRes = rr.getResource(auditCategoryPath);
if (auditCatRootRes != null) {
String[] pathParts = contentPath.split("/");
Resource currentRes = auditCatRootRes;
for (String part : pathParts) {
if(StringUtils.isEmpty(part)) {
//first part will be empty string, so skip it.
continue;
}
String nextPath = currentRes.getPath() + "/" + part;
Resource parentRes = currentRes;
currentRes = rr.getResource(nextPath);
if (currentRes == null) {
//create it
Map folderProps = new HashMap<>();
folderProps.put(JcrConstants.JCR_PRIMARYTYPE, JcrResourceConstants.NT_SLING_FOLDER);
currentRes = rr.create(parentRes, part, folderProps);
auditCategoryRes = currentRes;
rr.commit();
rr.refresh();
LOG.debug("created audit folder at {}", currentRes.getPath());
}
}
}
}
return auditCategoryRes;
}
private Resource getAuditCategoryResource(ResourceResolver rr, String auditCategory, String contentPath) {
final StringBuilder auditCategoryPath = new StringBuilder(AUDIT_ROOT);
auditCategoryPath.append("/").append(auditCategory.replace('/', '.'));
auditCategoryPath.append(contentPath);
return rr.getResource(auditCategoryPath.toString());
}
private void moveAudits(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
node.visit(childNode -> {
moveAuditsForChildNode(manager, rr, childNode);
});
});
});
}
private void moveAuditsForChildNode(ActionManager manager, ResourceResolver rr, MovingNode childNode) {
manager.deferredWithResolver(rr2 -> {
if(childNode.isAuditableMove()) {
String[] categories = auditLog.getCategories();
moveAuditsForChildNodeCategories(rr, childNode, rr2, categories);
}
});
}
private void moveAuditsForChildNodeCategories(ResourceResolver rr, MovingNode childNode, ResourceResolver rr2, String[] categories) throws PersistenceException, RepositoryException {
int movedCount = 0;
for (String auditCategory : categories) {
Resource sourceAuditRes = getAuditCategoryResource(rr, auditCategory, childNode.getSourcePath());
if (sourceAuditRes != null) {
Resource destAuditRes = getAuditCategoryResource(rr, auditCategory, childNode.getDestinationPath());
if(destAuditRes!=null) {
Iterator resourceIterator = sourceAuditRes.listChildren();
LOG.debug("moving audit entries for category {} from {} to {}", auditCategory, childNode.getSourcePath(), childNode.getDestinationPath());
while (resourceIterator.hasNext()) {
Resource auditEntry = resourceIterator.next();
rr2.move(auditEntry.getPath(), destAuditRes.getPath());
rr2.commit();
rr2.refresh();
LOG.debug("moved entry {} to {}", auditEntry.getPath(), destAuditRes.getPath());
movedCount++;
}
} else {
throw new RepositoryException("destination audit resource failed to create for category " + auditCategory + childNode.getDestinationPath());
}
}
}
note(childNode.getSourcePath(), Report.moved_audit_entries, movedCount);
}
protected void identifyStructure(ActionManager manager) {
manager.deferredWithResolver(rr -> {
AtomicInteger visitedSourceNodes = new AtomicInteger();
movePaths.forEach((source, dest) -> {
manager.deferredWithResolver(rr2 -> {
Resource res = rr2.getResource(source);
Optional rootNode = buildMoveNode(res);
if (rootNode.isPresent()) {
identifyStructureFromRoot(visitedSourceNodes, source, dest, rr2, res, rootNode.get());
}
});
});
});
}
private void identifyStructureFromRoot(AtomicInteger visitedSourceNodes, String source, String dest, ResourceResolver rr, Resource res, MovingNode root) throws TraversalException {
root.setDestinationPath(dest);
if (root instanceof MovingAsset) {
String destFolder = StringUtils.substringBeforeLast(dest, "/");
if (!additionalTargetFolders.contains(destFolder) && rr.getResource(destFolder) == null) {
additionalTargetFolders.add(destFolder);
}
}
moves.add(root);
note(source, Report.misc, "Root path");
note(source, Report.target, dest);
TreeFilteringResourceVisitor visitor = new TreeFilteringResourceVisitor(
JcrConstants.NT_FOLDER,
JcrResourceConstants.NT_SLING_FOLDER,
JcrResourceConstants.NT_SLING_ORDERED_FOLDER,
NameConstants.NT_PAGE
);
visitor.setResourceVisitorChecked((r, level) -> buildMoveTree(r, level, root, visitedSourceNodes));
visitor.setLeafVisitorChecked((r, level) -> buildMoveTree(r, level, root, visitedSourceNodes));
visitor.accept(res);
note("All scanned nodes", Report.misc, "Scanned " + visitedSourceNodes.get() + " source nodes.");
}
private void buildMoveTree(Resource r, int level, MovingNode root, AtomicInteger visitedSourceNodes) throws RepositoryException {
if (level > 0) {
Actions.setCurrentItem(r.getPath());
Optional node = buildMoveNode(r);
if (node.isPresent()) {
MovingNode childNode = node.get();
String parentPath = StringUtils.substringBeforeLast(r.getPath(), "/");
MovingNode parent = root.findByPath(parentPath)
.orElseThrow(() -> new RepositoryException("Unable to find data structure for node " + parentPath));
parent.addChild(childNode);
if (detailedReport) {
note(childNode.getSourcePath(), Report.target, childNode.getDestinationPath());
}
visitedSourceNodes.addAndGet(1);
}
}
}
private Optional buildMoveNode(Resource res) throws RepositoryException {
String type = res.getValueMap().get(JCR_PRIMARYTYPE, String.class);
MovingNode node = null;
switch (type) {
case JcrConstants.NT_FOLDER:
case JcrResourceConstants.NT_SLING_FOLDER:
case JcrResourceConstants.NT_SLING_ORDERED_FOLDER:
node = new MovingFolder();
break;
case NameConstants.NT_PAGE:
node = new MovingPage(pageManagerFactory);
break;
case DamConstants.NT_DAM_ASSET:
node = new MovingAsset();
break;
case JcrConstants.NT_UNSTRUCTURED:
if (res.getName().equals(JcrConstants.JCR_CONTENT)) {
return Optional.empty();
} else {
node = new MovingResource();
}
break;
case "cq:CommentAttachment":
node = new MovingResource();
break;
case AccessControlConstants.NT_REP_ACL:
node = new MovingResource();
break;
case "cq:PageContent":
// Page content is moved with the page, so ignore it here
break;
case TagConstants.NT_TAG:
default:
throw new RepositoryException("Type " + type + " is not supported at this time!");
}
if (node == null) {
return Optional.empty();
} else {
node.setSourcePath(res.getPath());
return Optional.of(node);
}
}
public void findReferences(ResourceResolver rr, MovingNode node) throws IllegalAccessException {
node.findReferences(rr, referenceSearchRoot, maxReferences);
}
protected void identifyReferences(ActionManager manager) {
AtomicInteger discoveredReferences = new AtomicInteger();
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
manager.deferredWithResolver(rr2 -> {
node.visit(childNode -> {
if (childNode.isSupposedToBeReferenced()) {
manager.deferredWithResolver(rr3 -> {
Actions.setCurrentItem("Looking for references to " + childNode.getSourcePath());
findReferences(rr3, childNode);
discoveredReferences.addAndGet(childNode.getAllReferences().size());
if (detailedReport) {
note(childNode.getSourcePath(), Report.all_references, childNode.getAllReferences().size());
note(childNode.getSourcePath(), Report.referred_in,childNode.getAllReferences().toString());
note(childNode.getSourcePath(), Report.published_references, childNode.getPublishedReferences().size());
}
});
}
});
});
});
});
manager.onFinish(() -> {
note("All discovered references", Report.misc, "Discovered " + discoveredReferences.get() + " references.");
});
}
protected void validateAllAcls(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
manager.deferredWithResolver(rr2 -> {
node.visit(childNode -> {
manager.deferredWithResolver(rr3 -> {
validateAcls(childNode, rr3);
});
});
});
});
if(auditTrails) {
checkNodeAcls(rr, AUDIT_ROOT, requiredAuditPrivileges);
}
});
}
private void validateAcls(MovingNode childNode, ResourceResolver rr3) throws RepositoryException {
try {
Actions.setCurrentItem("Checking ACLs on " + childNode.getSourcePath());
checkNodeAcls(rr3, childNode.getSourcePath(), requiredMovePrivileges);
for (String ref : childNode.getAllReferences()) {
Actions.setCurrentItem("Checking ACLs on " + ref + " which references " + childNode.getSourcePath());
validateAclsForReference(childNode, rr3, ref);
}
if (detailedReport) {
note(childNode.getSourcePath(), Report.acl_check, "Passed");
}
} catch (Exception e) {
note(childNode.getSourcePath(), Report.acl_check, "Failed");
throw e;
}
}
private void validateAclsForReference(MovingNode childNode, ResourceResolver rr, String ref) throws RepositoryException {
if (publishMethod != PublishMethod.NONE
&& childNode.getPublishedReferences().contains(ref)) {
checkNodeAcls(rr, childNode.getSourcePath(), requiredPublishPrivileges);
} else {
checkNodeAcls(rr, childNode.getSourcePath(), requiredUpdatePrivileges);
}
}
// Try to create as much of the folder structures ahead of time (for assets, etc)
protected void buildStructures(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
manager.deferredWithResolver(rr2 -> {
node.visit(childNode -> {
manager.deferredWithResolver(rr3 -> {
Actions.setCurrentItem("Building structure for " + childNode.getSourcePath());
childNode.move(replicatorQueue, rr3);
});
}, null, MovingNode::isCopiedBeforeMove);
});
});
additionalTargetFolders.forEach(path -> {
manager.deferredWithResolver(rr2 -> {
Actions.setCurrentItem("Building structure for " + path);
performNecessaryReplicationOnAncestors(rr2, path);
ResourceUtil.getOrCreateResource(rr2, path, Collections.EMPTY_MAP, "sling:Folder", false);
if (detailedReport) {
note(path, Report.misc, "Created additional destination folder");
}
});
});
});
}
// Move assets and pages, and in some cases folders that were not already moved in the previous step
protected void moveTree(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
node.visit(childNode -> {
if (!childNode.isCopiedBeforeMove() || !resourceExists(rr, childNode.getDestinationPath())) {
Actions.setCurrentItem("Moving " + childNode.getSourcePath());
try {
childNode.move(replicatorQueue, rr);
} catch (IllegalAccessException | MovingException e) {
LOG.error("Fatal uncaught error in moveTree {}", e);
}
}
});
});
});
}
protected void activateTreeStructure(ActionManager manager) {
manager.deferredWithResolver(rr -> {
moves.forEach(node -> {
manager.deferredWithResolver(rr2 -> {
node.visit(childNode -> {
manager.deferredWithResolver(rr3 -> {
Actions.setCurrentItem("Replicating " + childNode.getDestinationPath());
performNecessaryReplication(rr3, childNode.getDestinationPath());
});
}, null, MovingNode::isCopiedBeforeMove);
});
});
});
}
protected void activateNew(ActionManager step3) {
step3.deferredWithResolver(rr -> {
getAllActivationPaths().filter(this::isActivationPath)
.forEach(path -> {
step3.deferredWithResolver(rr2 -> {
Actions.setCurrentItem("Replicating " + path);
performNecessaryReplication(rr2, path);
});
});
});
}
protected void activateReferences(ActionManager step4) {
step4.deferredWithResolver(rr -> {
getAllReplicationPaths().filter(this::isForeignPath)
.forEach(path -> {
step4.deferredWithResolver(rr2 -> {
Actions.setCurrentItem("Replicating references " + path);
performNecessaryReplication(rr2, path);
});
});
});
}
protected void deactivateOld(ActionManager step5) {
step5.deferredWithResolver(rr -> {
getAllReplicationPaths().filter(this::isDeactivationPath)
.forEach(path -> {
step5.deferredWithResolver(rr2 -> {
Actions.setCurrentItem("Deactivating " + path);
performNecessaryReplication(rr2, path);
});
});
});
}
protected boolean isDeactivationPath(String path) {
boolean result = false;
for (Map.Entry mapping : movePaths.entrySet()) {
String sourcePath = mapping.getKey();
String destinationPath = mapping.getValue();
if (path.startsWith(sourcePath)) {
result = true;
} else if (path.startsWith(destinationPath)) {
return false;
}
}
return result;
}
protected boolean isActivationPath(String path) {
return !isDeactivationPath(path);
}
protected boolean isForeignPath(String path) {
for (Map.Entry mapping : movePaths.entrySet()) {
String sourcePath = mapping.getKey();
String destinationPath = mapping.getValue();
if (path.startsWith(sourcePath) || path.startsWith(destinationPath)) {
return false;
}
}
return true;
}
protected void removeSource(ActionManager manager) {
manager.deferredWithResolver(rr -> {
for (MovingNode node : moves) {
//TODO: DOUBLE-CHECK NOT TO DELETE ANYTHING?
rr.delete(rr.resolve(node.getSourcePath()));
}
});
}
@SuppressWarnings("squid:S00115")
enum Report {
misc, target, acl_check, all_references, published_references, moved_audit_entries, move_time, activate_time, deactivate_time, referred_in
}
private final Map> reportData = new LinkedHashMap<>();
private void note(String page, Report col, Object value) {
synchronized (reportData) {
if (!reportData.containsKey(page)) {
reportData.put(page, new EnumMap<>(Report.class));
}
reportData.get(page).put(col, value);
}
}
@Override
public void storeReport(ProcessInstance instance, ResourceResolver rr) throws RepositoryException, PersistenceException {
GenericReport report = new GenericReport();
report.setRows(reportData, SOURCE_COL, Report.class);
report.persist(rr, instance.getPath() + "/jcr:content/report");
}
private Privilege[] getPrivilegesFromNames(ResourceResolver res, String[] names) throws RepositoryException {
Session session = res.adaptTo(Session.class);
AccessControlManager acm = session.getAccessControlManager();
Privilege[] prvlgs = new Privilege[names.length];
for (int i = 0; i < names.length; i++) {
prvlgs[i] = acm.privilegeFromName(names[i]);
}
return prvlgs;
}
public void checkNodeAcls(ResourceResolver res, String path, Privilege[] prvlgs) throws RepositoryException {
Actions.setCurrentItem(path);
Session session = res.adaptTo(Session.class);
boolean report = res.getResource(path).getResourceType().equals(NameConstants.NT_PAGE);
if (!session.getAccessControlManager().hasPrivileges(path, prvlgs)) {
note(path, Report.acl_check, "FAIL");
throw new RepositoryException("Insufficient permissions to permit move operation");
} else if (report) {
note(path, Report.acl_check, "PASS");
}
}
private String reversePathLookup(String path) {
for (Map.Entry mapping : movePaths.entrySet()) {
String sourcePath = mapping.getKey();
String destinationPath = mapping.getValue();
if (path.startsWith(destinationPath)) {
return path.replaceAll(Pattern.quote(destinationPath), sourcePath);
} else {
return path;
}
}
return null;
}
private Stream getAllActivationPaths() {
Set allPaths = new TreeSet<>();
moves.forEach((n) -> {
n.visit(node -> {
allPaths.addAll(node.getPublishedReferences());
});
});
allPaths.addAll(replicatorQueue.getActivateOperations().keySet());
return allPaths.stream();
}
private Stream getAllReplicationPaths() {
return Stream.concat(
replicatorQueue.getActivateOperations().keySet().stream(),
replicatorQueue.getDeactivateOperations().keySet().stream()
).distinct();
}
private void performNecessaryReplication(ResourceResolver rr, String path) throws ReplicationException {
ReplicationActionType action;
boolean isDeactivation = isDeactivationPath(path);
if (isDeactivation) {
action = ReplicationActionType.DEACTIVATE;
} else {
action = ReplicationActionType.ACTIVATE;
}
long start = System.currentTimeMillis();
if (!dryRun) {
replicator.replicate(rr.adaptTo(Session.class), action, path);
}
long end = System.currentTimeMillis();
if (isDeactivation) {
note(path, Report.deactivate_time, end - start);
} else {
note(reversePathLookup(path), Report.activate_time, end - start);
}
}
private void performNecessaryReplicationOnAncestors(ResourceResolver rr, String path) throws ReplicationException {
String checkPath = "";
for (String part : path.split(Pattern.quote("/"))) {
if (part.isEmpty()) {
continue;
}
checkPath += "/" + part;
if (rr.getResource(checkPath) == null) {
performNecessaryReplication(rr, checkPath);
}
}
}
}