com.adobe.acs.commons.mcp.impl.processes.FolderRelocator.java Source code

Java tutorial

Introduction

Here is the source code for com.adobe.acs.commons.mcp.impl.processes.FolderRelocator.java

Source

/*
 * #%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;

import com.adobe.acs.commons.fam.Failure;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.fam.actions.ActionBatch;
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.PathfieldComponent;
import com.adobe.acs.commons.mcp.form.RadioComponent;
import com.adobe.acs.commons.util.visitors.SimpleFilteringResourceVisitor;
import com.adobe.acs.commons.util.visitors.TreeFilteringResourceVisitor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
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 com.adobe.acs.commons.mcp.form.FormField;
import java.io.Serializable;
import org.apache.commons.lang3.StringUtils;

/**
 * This utility takes an alternate approach to moving folders using a four-step
 * process. This can be used to move one or more folders as needed.
 * <ul>
 * <li>Step 1: Evaluate the requirements, check for possible authorization
 * issues; Abort sequence halts other work</li>
 * <li>Step 2: Prepare destination folder structure; Abort sequence is to remove
 * any folders created already</li>
 * <li>Step 3: Relocate the contents of the folders</li>
 * <li>Step 4: Remove the old folder structures</li>
 * </ul>
 *
 * There are different combinations of how this can be used:
 * <ul>
 * <li>Rename a folder, keeping it where it is presently located. This uses the
 * Rename mode where the source is the folder path and the destination is the
 * complete path of the folder as it should be after renaming</li>
 * <li>Move a folder, keeping its name intact. This uses the Move mode where the
 * source is the folder path and the destination is the parent node where it
 * should go. You can also use the RENAME mode for this, provided that the
 * destination path also specifies the node name. They're technically the same
 * thing, but MOVE is provided for convenience.</li>
 * <li>Move multiple folders, keeping all names intact. This uses an alternate
 * constructor which takes a list of paths as sources and a single destination
 * for the parent where all folders will be moved to. This is functionally the
 * same as moving all folders in a loop, except that the operation is batched
 * together as one big process instead of having to define each folder move as a
 * separate process.</li>
 * </ul>
 */
public class FolderRelocator extends ProcessDefinition implements Serializable {
    private static final long serialVersionUID = 7526472295622776160L;

    public enum Mode {
        RENAME, MOVE
    }

    private Map<String, String> sourceToDestination;
    @FormField(name = "Source folder(s)", description = "One or more source folders must be provided.  Multiple folders implies a move operation.", hint = "/content/dam/someFolder", component = PathfieldComponent.FolderSelectComponent.class, options = {
            "base=/content/dam", "multiple" })
    private String[] sourcePaths;
    @FormField(name = "Destination folder", description = "Destination parent for move, or destination parent folder plus new name for rename", hint = "Move: /content/dam/moveToFolder | Rename: /content/dam/moveToFolder/newName", component = PathfieldComponent.FolderSelectComponent.class, options = {
            "base=/content/dam" })
    private String destinationPath;
    @FormField(name = "Mode", description = "Move relocates one or more folders.  Rename relocates and takes the last part of the path as the new name for one folder.", required = false, component = RadioComponent.EnumerationSelector.class, options = {
            "horizontal", "default=MOVE" })
    private Mode mode;

    private final transient String[] requiredFolderPrivilegeNames = { Privilege.JCR_READ, Privilege.JCR_WRITE,
            Privilege.JCR_REMOVE_CHILD_NODES, Privilege.JCR_REMOVE_NODE };

    private final transient String[] requiredNodePrivilegeNames = { Privilege.JCR_ALL };

    private transient Privilege[] requiredFolderPrivileges;
    private transient Privilege[] requiredNodePrivileges;

    private int batchSize = 5;

    public FolderRelocator() {
        // Invoked when starting via the MCP servlet
    }

    /**
     * Prepare a folder relocation for multiple folders to be moved under the
     * same target parent node. Because there are multiple folders being moved
     * under one parent, this assumes the operation is a Move not a Rename.
     *
     * @param sourcePaths List of source paths to move
     * @param destinationPath Destination parent path
     */
    public FolderRelocator(String[] sourcePaths, String destinationPath) {
        init(sourcePaths, destinationPath);
    }

    /**
     * Prepare a folder relocation for a single folder.
     *
     * @param sourcePath Source node to be moved
     * @param destinationPath Destination path, which is either the parent (if
     * mode is MOVE) or the desired final path for the node (if mode is RENAME)
     * @param processMode MOVE if node name stays the same and needs to be under
     * a new parent; RENAME if the node needs to change its name and destination
     * contains that new name.
     */
    public FolderRelocator(String sourcePath, String destinationPath, Mode processMode) {
        init(sourcePath, destinationPath, processMode);
    }

    @Override
    public void init() throws RepositoryException {
        if (sourcePaths != null && sourcePaths.length == 1) {
            init(sourcePaths[0], destinationPath, mode);
        } else {
            init(sourcePaths, destinationPath);
        }
    }

    private void init(String[] sourcePaths, String destinationPath) {
        sourceToDestination = new HashMap<>();
        this.mode = Mode.MOVE;

        for (String sourcePath : sourcePaths) {
            String nodeName = sourcePath.substring(sourcePath.lastIndexOf('/'));
            String destination = destinationPath + nodeName;
            sourceToDestination.put(sourcePath, destination);
        }
    }

    private void init(String sourcePath, String destinationPath, Mode processMode) {
        sourceToDestination = new HashMap<>();
        this.mode = processMode;

        String destination = destinationPath;
        if (mode == Mode.MOVE) {
            String nodeName = sourcePath.substring(sourcePath.lastIndexOf('/'));
            destination += nodeName;
        }
        sourceToDestination.put(sourcePath, destination);
    }

    /**
     * Batch size determines the number of operations (folder creation or node
     * moves) performed at a time.
     *
     * @param batchSize the batchSize to set
     */
    public void setBatchSize(int batchSize) {
        this.batchSize = batchSize;
    }

    @Override
    public void buildProcess(ProcessInstance instance, ResourceResolver rr)
            throws LoginException, RepositoryException {
        validateInputs(rr);
        Session ses = rr.adaptTo(Session.class);
        requiredFolderPrivileges = getPrivilegesFromNames(ses, requiredFolderPrivilegeNames);
        requiredNodePrivileges = getPrivilegesFromNames(ses, requiredNodePrivilegeNames);
        instance.defineCriticalAction("Validate ACLs", rr, this::validateAllAcls);
        instance.defineCriticalAction("Build target folders", rr, this::buildTargetFolders)
                .onFailure(this::abortStep2);
        instance.defineCriticalAction("Move nodes", rr, this::moveNodes);
        instance.defineCriticalAction("Remove old folders", rr, this::removeSourceFolders);
        if (sourcePaths.length > 1) {
            instance.getInfo().setDescription("Move " + sourcePaths.length + " folders to " + destinationPath);
        } else {
            String verb = StringUtils.capitalize(mode.name().toLowerCase());
            instance.getInfo().setDescription(verb + " " + sourcePaths[0] + " to " + destinationPath);
        }
    }

    @SuppressWarnings("squid:S3776")
    private void validateInputs(ResourceResolver res) throws RepositoryException {
        Optional<RepositoryException> error = sourceToDestination.entrySet().stream().map((pair) -> {
            String entrySourcePath = pair.getKey();
            String entryDestinationPath = pair.getValue();
            if (entrySourcePath == null) {
                return new RepositoryException("Source path should not be null");
            }
            if (entryDestinationPath == null) {
                return new RepositoryException("Destination path should not be null");
            }
            if (entryDestinationPath.contains(entrySourcePath + "/")) {
                return new RepositoryException("Destination must be outside of source folder");
            }
            if (!resourceExists(res, entrySourcePath)) {
                if (!entrySourcePath.startsWith("/")) {
                    return new RepositoryException(
                            "Paths are not valid unless they start with a forward slash, you provided: "
                                    + entrySourcePath);
                } else {
                    return new RepositoryException("Unable to find source " + entrySourcePath);
                }
            }
            if (!resourceExists(res, entryDestinationPath.substring(0, entryDestinationPath.lastIndexOf('/')))) {
                if (!entryDestinationPath.startsWith("/")) {
                    return new RepositoryException(
                            "Paths are not valid unless they start with a forward slash, you provided: "
                                    + entryDestinationPath);
                } else {
                    return new RepositoryException("Unable to find destination " + entryDestinationPath);
                }
            }
            return null;
        }).filter(Objects::nonNull).findFirst();
        if (error.isPresent()) {
            Logger.getLogger(FolderRelocator.class.getName()).log(Level.SEVERE,
                    "Validation error prior to starting move operations: {0}", error.get().getMessage());
            throw error.get();
        }
    }

    private boolean resourceExists(ResourceResolver rr, String path) {
        Resource res = rr.getResource(path);
        return res != null && !Resource.RESOURCE_TYPE_NON_EXISTING.equals(res.getResourceType());
    }

    private void validateAllAcls(ActionManager step1) {
        TreeFilteringResourceVisitor folderVisitor = new TreeFilteringResourceVisitor();
        folderVisitor.setBreadthFirstMode();
        folderVisitor.setResourceVisitor((resource, level) -> step1
                .deferredWithResolver(rr -> checkNodeAcls(rr, resource.getPath(), requiredFolderPrivileges)));
        folderVisitor.setLeafVisitor((resource, level) -> step1
                .deferredWithResolver(rr -> checkNodeAcls(rr, resource.getPath(), requiredNodePrivileges)));
        sourceToDestination.keySet().forEach(sourcePath -> beginStep(step1, sourcePath, folderVisitor));
    }

    private Privilege[] getPrivilegesFromNames(Session session, String[] names) throws RepositoryException {
        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;
    }

    private void checkNodeAcls(ResourceResolver res, String path, Privilege[] prvlgs) throws RepositoryException {
        Actions.setCurrentItem(path);
        Session session = res.adaptTo(Session.class);
        if (!session.getAccessControlManager().hasPrivileges(path, prvlgs)) {
            throw new RepositoryException("Insufficient permissions to permit move operation");
        }
    }

    private void buildTargetFolders(ActionManager step2) {
        TreeFilteringResourceVisitor folderVisitor = new TreeFilteringResourceVisitor();
        folderVisitor.setBreadthFirstMode();
        folderVisitor.setResourceVisitor((res, level) -> {
            String path = res.getPath();
            step2.deferredWithResolver(Actions.retry(5, 100, rr -> buildDestinationFolder(rr, path)));
        });
        sourceToDestination.keySet().forEach(sourcePath -> beginStep(step2, sourcePath, folderVisitor));
    }

    private void abortStep2(List<Failure> errors, ResourceResolver rr) {
        Logger.getLogger(FolderRelocator.class.getName()).log(Level.SEVERE,
                "{0} issues enountered trying to create destination folder structure; aborting process.",
                errors.size());
        sourceToDestination.keySet().forEach(sourcePath -> {
            try {
                rr.delete(rr.getResource(convertSourceToDestination(sourcePath)));
            } catch (PersistenceException | RepositoryException ex) {
                rr.refresh();
                Logger.getLogger(FolderRelocator.class.getName()).log(Level.SEVERE, null, ex);
            }
        });
    }

    private void buildDestinationFolder(ResourceResolver rr, String sourceFolder)
            throws PersistenceException, RepositoryException, InterruptedException {
        Session session = rr.adaptTo(Session.class);
        session.getWorkspace().getObservationManager().setUserData("changedByWorkflowProcess");
        Resource source = rr.getResource(sourceFolder);
        String targetPath = convertSourceToDestination(sourceFolder);
        if (!resourceExists(rr, targetPath)) {
            Actions.setCurrentItem(sourceFolder + "->" + targetPath);
            String targetParentPath = targetPath.substring(0, targetPath.lastIndexOf('/'));
            String targetName = targetPath.substring(targetPath.lastIndexOf('/') + 1);
            waitUntilResourceFound(rr, targetParentPath);
            Resource destParent = rr.getResource(targetParentPath);
            Logger.getLogger(FolderRelocator.class.getName()).log(Level.INFO, "Creating target for {0}",
                    sourceFolder);
            rr.create(destParent, targetName, source.getValueMap());
            rr.commit();
            rr.refresh();
        }
        String sourceJcrContent = sourceFolder + "/jcr:content";
        if (resourceExists(rr, sourceJcrContent)) {
            Actions.getCurrentActionManager().deferredWithResolver(Actions.retry(5, 50, (rrr) -> {
                if (!resourceExists(rrr, targetPath + "/jcr:content")) {
                    waitUntilResourceFound(rrr, targetPath);
                    rrr.copy(sourceJcrContent, targetPath);
                    rrr.commit();
                    rrr.refresh();
                }
            }));
        }
    }

    private void waitUntilResourceFound(ResourceResolver rr, String path)
            throws InterruptedException, RepositoryException {
        for (int i = 0; i < 10; i++) {
            if (resourceExists(rr, path)) {
                return;
            }
            Thread.sleep(100);
            rr.refresh();
        }
        throw new RepositoryException("Resource not found: " + path);
    }

    private String convertSourceToDestination(String path) throws RepositoryException {
        return sourceToDestination.entrySet().stream().filter(entry -> path.startsWith(entry.getKey())).findFirst()
                .map(entry -> path.replaceAll(Pattern.quote(entry.getKey()), entry.getValue()))
                .orElseThrow(() -> new RepositoryException("Cannot determine destination for " + path));
    }

    private void moveNodes(ActionManager step3) {
        ActionBatch batch = new ActionBatch(step3, batchSize);
        batch.setRetryCount(10);
        TreeFilteringResourceVisitor folderVisitor = new TreeFilteringResourceVisitor();
        folderVisitor.setBreadthFirstMode();
        folderVisitor.setLeafVisitor((res, level) -> {
            String path = res.getPath();
            if (!path.endsWith("jcr:content")) {
                batch.add(rr -> moveItem(rr, path));
            }
        });
        sourceToDestination.keySet().forEach(sourcePath -> beginStep(step3, sourcePath, folderVisitor));
        batch.commitBatch();
    }

    private void moveItem(ResourceResolver rr, String path) throws RepositoryException {
        Logger.getLogger(FolderRelocator.class.getName()).log(Level.INFO, "Moving {0}", path);
        Actions.setCurrentItem(path);
        Session session = rr.adaptTo(Session.class);
        // Inhibits some workflows
        session.getWorkspace().getObservationManager().setUserData("changedByWorkflowProcess");
        session.move(path, convertSourceToDestination(path));
    }

    private void removeSourceFolders(ActionManager step4) {
        sourceToDestination.keySet().forEach(sourcePath -> step4
                .deferredWithResolver(Actions.retry(5, 100, rr -> deleteResource(rr, sourcePath))));
    }

    private void deleteResource(ResourceResolver rr, String path) throws PersistenceException {
        Actions.setCurrentItem(path);
        rr.delete(rr.getResource(path));
    }

    private void beginStep(ActionManager step, String startingNode, SimpleFilteringResourceVisitor visitor) {
        try {
            step.withResolver(rr -> visitor.accept(rr.getResource(startingNode)));
        } catch (Exception ex) {
            Logger.getLogger(FolderRelocator.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void storeReport(ProcessInstance instance, ResourceResolver rr) {
        // no-op
    }
}