Java tutorial
/* * #%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.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.FormField; import com.adobe.acs.commons.mcp.form.PathfieldComponent; 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.SimpleFilteringResourceVisitor; import com.adobe.acs.commons.util.visitors.TreeFilteringResourceVisitor; import com.day.cq.replication.ReplicationActionType; import com.day.cq.replication.ReplicationException; import com.day.cq.replication.ReplicationOptions; import com.day.cq.replication.ReplicationStatus; import com.day.cq.replication.Replicator; import com.day.cq.wcm.api.NameConstants; import com.day.cq.wcm.api.PageManager; import com.day.cq.wcm.api.PageManagerFactory; import com.day.cq.wcm.commons.ReferenceSearch; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; 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.reflect.FieldUtils; 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; /** * Relocate Pages and/or Sites using a parallelized move process */ public class PageRelocator extends ProcessDefinition { public PageRelocator(PageManagerFactory pageManagerFactory, Replicator replicator) { this.pageManagerFactory = pageManagerFactory; this.replicator = replicator; } private final PageManagerFactory pageManagerFactory; private final Replicator replicator; public enum Mode { RENAME, MOVE } public enum PublishMethod { NONE, SELF_MANAGED, QUEUE } @FormField(name = "Source page", description = "Select page/site to be moved", hint = "/content/my-site/en/my-page", component = PathfieldComponent.PageSelectComponent.class, options = { "base=/content" }) private String sourcePath; @FormField(name = "Destination", description = "Destination parent for move, or destination parent folder plus new name for rename", hint = "Move: /content/my-site/some-page | Rename: /content/my-site/some-page/new-name", component = PathfieldComponent.PageSelectComponent.class, options = { "base=/content" }) private String destinationPath; @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 = "Mode", description = "Move relocates the page keeping the original name. Rename changes the name, optionally moving the page.", required = false, component = RadioComponent.EnumerationSelector.class, options = { "horizontal", "default=MOVE" }) private Mode mode; @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.", required = false, component = RadioComponent.EnumerationSelector.class, options = { "horizontal", "default=SELF_MANAGED" }) public PublishMethod publishMethod; @FormField(name = "Create versions", description = "Create versions for anything being 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; private final transient String[] requiredPrivilegeNames = { Privilege.JCR_READ, Privilege.JCR_WRITE, Privilege.JCR_REMOVE_CHILD_NODES, Privilege.JCR_REMOVE_NODE, Replicator.REPLICATE_PRIVILEGE }; Privilege[] requiredPrivileges; ReplicatorQueue replicatorQueue = new ReplicatorQueue(); ReplicationOptions replicationOptions; @Override public void init() throws RepositoryException { if (mode == Mode.MOVE) { String nodeName = sourcePath.substring(sourcePath.lastIndexOf('/')); destinationPath += nodeName; } 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 (sourcePath == null) { throw new RepositoryException("Source path should not be null"); } if (destinationPath == null) { throw new RepositoryException("Destination path should not be null"); } 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 { throw new RepositoryException("Unable to find destination " + destinationPath); } } } ManagedProcess instanceInfo; @Override public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException, RepositoryException { validateInputs(rr); instanceInfo = instance.getInfo(); String desc = dryRun ? "DRY RUN: " : ""; desc += mode.name().toLowerCase() + " " + sourcePath + " to " + destinationPath; desc += "; Publish mode " + publishMethod.name().toLowerCase(); instance.getInfo().setDescription(desc); requiredPrivileges = getPrivilegesFromNames(rr, requiredPrivilegeNames); instance.defineCriticalAction("Check ACLs", rr, this::validateAllAcls); instance.defineAction("Move Pages", rr, this::movePages); if (publishMethod != PublishMethod.NONE) { instance.defineAction("Activate New", rr, this::activateNew); instance.defineAction("Activate References", rr, this::activateReferences); instance.defineAction("Deactivate Old", rr, this::deactivateOld); } instance.defineAction("Remove old pages", rr, this::removeSource); } protected void validateAllAcls(ActionManager step1) { SimpleFilteringResourceVisitor pageVisitor; if (extensiveACLChecks) { pageVisitor = new SimpleFilteringResourceVisitor(); pageVisitor.setLeafVisitor((resource, level) -> step1 .deferredWithResolver(rr -> checkNodeAcls(rr, resource.getPath(), requiredPrivileges))); } else { pageVisitor = new TreeFilteringResourceVisitor(NameConstants.NT_PAGE); } pageVisitor.setBreadthFirstMode(); pageVisitor.setResourceVisitor((resource, level) -> step1 .deferredWithResolver(rr -> checkNodeAcls(rr, resource.getPath(), requiredPrivileges))); beginStep(step1, sourcePath, pageVisitor); } protected void movePages(ActionManager step2) { TreeFilteringResourceVisitor pageVisitor = new TreeFilteringResourceVisitor(NameConstants.NT_PAGE); pageVisitor.setBreadthFirstMode(); pageVisitor.setResourceVisitor( (resource, level) -> step2.deferredWithResolver(rr -> movePage(rr, resource.getPath()))); beginStep(step2, sourcePath, pageVisitor); } protected void activateNew(ActionManager step3) { step3.deferredWithResolver(rr -> { getAllReplicationPaths().filter(p -> p.startsWith(destinationPath) && !p.startsWith(sourcePath)) .forEach(path -> { step3.deferredWithResolver(rr2 -> { performNecessaryReplication(rr2, path); }); }); }); } protected void activateReferences(ActionManager step4) { step4.deferredWithResolver(rr -> { getAllReplicationPaths().filter(p -> !p.startsWith(destinationPath) && !p.startsWith(sourcePath)) .forEach(path -> { step4.deferredWithResolver(rr2 -> { performNecessaryReplication(rr2, path); }); }); }); } protected void deactivateOld(ActionManager step5) { step5.deferredWithResolver(rr -> { getAllReplicationPaths().filter(p -> p.startsWith(sourcePath)).forEach(path -> { step5.deferredWithResolver(rr2 -> { performNecessaryReplication(rr2, path); }); }); }); } protected void removeSource(ActionManager step6) { if (instanceInfo.getReportedErrorsList().isEmpty() && !dryRun) { step6.deferredWithResolver(rr -> { rr.delete(rr.getResource(sourcePath)); }); } } 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); } } @SuppressWarnings("squid:S00115") enum Report { target, acl_check, all_references, published_references, move_time, activate_time, deactivate_time } private final Map<String, EnumMap<Report, Object>> reportData = new TreeMap<>(); 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", Report.class); report.persist(rr, instance.getPath() + "/jcr:content/report"); } //--- Utility functions 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 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 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; } private 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"); } } @SuppressWarnings("squid:S00112") private void movePage(ResourceResolver rr, String sourcePage) throws Exception { PageManager manager = pageManagerFactory.getPageManager(rr); Field replicatorField = FieldUtils.getDeclaredField(manager.getClass(), "replicator", true); FieldUtils.writeField(replicatorField, manager, replicatorQueue); String destination = convertSourceToDestination(sourcePage); String destinationParent = destination.substring(0, destination.lastIndexOf('/')); note(sourcePage, Report.target, destination); String beforeName = ""; final long start = System.currentTimeMillis(); String contentPath = sourcePage + "/jcr:content"; List<String> refs = new ArrayList<>(); List<String> publishRefs = new ArrayList<>(); if (maxReferences != 0 && resourceExists(rr, contentPath)) { ReferenceSearch refSearch = new ReferenceSearch(); refSearch.setExact(true); refSearch.setHollow(true); refSearch.setMaxReferencesPerPage(maxReferences); refSearch.setSearchRoot(referenceSearchRoot); refSearch.search(rr, sourcePath).values().stream().peek(p -> refs.add(p.getPagePath())) .filter(p -> isActivated(rr, p.getPagePath())).map(ReferenceSearch.Info::getPagePath) .collect(Collectors.toCollection(() -> publishRefs)); } note(sourcePage, Report.all_references, refs.size()); note(sourcePage, Report.published_references, publishRefs.size()); if (!dryRun) { Actions.retry(10, 500, res -> { waitUntilResourceFound(res, destinationParent); Resource source = rr.getResource(sourcePage); if (resourceExists(res, contentPath)) { manager.move(source, destination, beforeName, true, true, listToStringArray(refs), listToStringArray(publishRefs)); } else { Map<String, Object> props = new HashMap<>(); Resource parent = res.getResource(destinationParent); res.create(parent, source.getName(), source.getValueMap()); } res.commit(); res.refresh(); source = rr.getResource(sourcePage); if (source != null && source.hasChildren()) { for (Resource child : source.getChildren()) { res.move(child.getPath(), destination); } res.commit(); } }).accept(rr); } long end = System.currentTimeMillis(); note(sourcePage, Report.move_time, end - start); } private String convertSourceToDestination(String path) { return path.replaceAll(Pattern.quote(sourcePath), destinationPath); } private String reversePathLookup(String path) { if (path.startsWith(destinationPath)) { return path.replaceAll(Pattern.quote(destinationPath), sourcePath); } else { return path; } } private Stream<String> getAllReplicationPaths() { Stream s1 = replicatorQueue.activateOperations.keySet().stream(); Stream s2 = replicatorQueue.deactivateOperations.keySet().stream(); return Stream.concat(s1, s2); } private void performNecessaryReplication(ResourceResolver rr, String path) throws ReplicationException { ReplicationActionType action; if (path.startsWith(sourcePath)) { 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 (path.startsWith(sourcePath)) { note(path, Report.deactivate_time, end - start); } else { note(reversePathLookup(path), Report.activate_time, end - start); } } private boolean isActivated(ResourceResolver rr, String path) { ReplicationStatus replicationStatus = rr.getResource(path).adaptTo(ReplicationStatus.class); return replicationStatus.isActivated(); } private String[] listToStringArray(List<String> values) { return values.toArray(new String[0]); } }