All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.acs.commons.mcp.impl.processes.renovator.Renovator Maven / Gradle / Ivy

There is a newer version: 6.6.0
Show newest version
/*
 * #%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);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy