Java tutorial
/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2014 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.replication.status.impl; import com.adobe.acs.commons.packaging.PackageHelper; import com.adobe.acs.commons.replication.status.ReplicationStatusManager; import com.adobe.acs.commons.util.ParameterUtil; import com.day.cq.jcrclustersupport.ClusterAware; import com.day.cq.replication.ReplicationAction; import com.day.cq.replication.ReplicationEvent; import com.day.cq.replication.ReplicationStatus; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.PropertyOption; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.jackrabbit.vault.packaging.JcrPackage; import org.apache.jackrabbit.vault.packaging.JcrPackageDefinition; import org.apache.jackrabbit.vault.packaging.PackageException; import org.apache.jackrabbit.vault.packaging.Packaging; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.sling.event.jobs.Job; import org.apache.sling.event.jobs.JobManager; import org.apache.sling.event.jobs.consumer.JobConsumer; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.RepositoryException; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @Component(label = "ACS AEM Commons - Package Replication Status Updater", description = "Event handler that listens for Jcr Package replications and updates the Replication Status of " + "its content accordingly.", metatype = true, immediate = true, policy = ConfigurationPolicy.REQUIRE) @Properties({ @Property(label = "Event Topics", value = { ReplicationAction.EVENT_TOPIC, ReplicationEvent.EVENT_TOPIC }, description = "[Required] Event Topics this event handler will to respond to.", name = EventConstants.EVENT_TOPIC, propertyPrivate = true), @Property(label = "Event Filters", value = "(" + ReplicationAction.PROPERTY_TYPE + "=ACTIVATE)", name = EventConstants.EVENT_FILTER, propertyPrivate = true), @Property(name = JobConsumer.PROPERTY_TOPICS, value = JcrPackageReplicationStatusEventHandler.JOB_TOPIC, propertyPrivate = true) }) @Service public class JcrPackageReplicationStatusEventHandler implements JobConsumer, EventHandler, ClusterAware { private static final Logger log = LoggerFactory.getLogger(JcrPackageReplicationStatusEventHandler.class); private static final String FALLBACK_REPLICATION_USER_ID = "Package Replication"; private static final String PROPERTY_PATHS = "paths"; private static final String PROPERTY_REPLICATED_BY = "replicatedBy"; private enum ReplicatedAt { CURRENT_TIME, PACKAGE_LAST_MODIFIED; } static final String[] DEFAULT_REPLICATION_STATUS_NODE_TYPES = { "cq:Page/cq:PageContent (?!/conf/.*/settings/wcm/templates/[^/]*/initial).*", // make sure to not cover initial content below editable templates "dam:AssetContent", "rep:User", "rep:Group", "sling:OrderedFolder/nt:unstructured", ReplicationStatus.NODE_TYPE, // replication status must be after cq:PageContent, because cq:PageContent is of mixin "cq:ReplicatonStatus" as well "nt:unstructured /conf/.*/settings/wcm/policies/.*" // cover policies below editable templates }; @Property(label = "Replication Status Node Type and Path Restrictions", description = "Node types that are candidates to update Replication Status on. Each item has the format '<nodetype-restriction> (<path-restriction>)'. The <path-restriction> is optional. The <nodetype-restriction> may be composed out of several node types separated by '/'.", cardinality = Integer.MAX_VALUE, value = { "cq:Page/cq:PageContent (?!/conf/.*/settings/wcm/templates/[^/]*/initial).*", // make sure to not cover initial content below editable templates "dam:AssetContent", "rep:User", "rep:Group", "sling:OrderedFolder/nt:unstructured", ReplicationStatus.NODE_TYPE, // replication status must be after cq:PageContent, because cq:PageContent is of mixin "cq:ReplicatonStatus" as well "nt:unstructured /conf/.*/settings/wcm/policies/.*" // cover policies below editable templates }) public static final String PROP_REPLICATION_STATUS_NODE_TYPES = "node-types"; /** * key = allowed node type (hierarchy), value = optional path restriction (may be null). */ private Map<String, Pattern> pathRestrictionByNodeType; protected static final String JOB_TOPIC = "acs-commons/replication/package"; @Reference private Packaging packaging; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private ReplicationStatusManager replicationStatusManager; @Reference private PackageHelper packageHelper; @Reference private JobManager jobManager; private boolean isMaster = false; // Previously "Package Replication" private static final String DEFAULT_REPLICATED_BY_OVERRIDE = ""; private String replicatedByOverride = DEFAULT_REPLICATED_BY_OVERRIDE; @Property(label = "'Replicated By' Override", description = "The 'user name' to set the 'replicated by' property to. If left blank the ACTUAL user that issued the package replication will be used. Defaults to blank.", value = DEFAULT_REPLICATED_BY_OVERRIDE) public static final String PROP_REPLICATED_BY_OVERRIDE = "replicated-by.override"; public static final String LEGACY_PROP_REPLICATED_BY_OVERRIDE = "replicated-by"; private static final ReplicatedAt DEFAULT_REPLICATED_AT = ReplicatedAt.PACKAGE_LAST_MODIFIED; private ReplicatedAt replicatedAt = DEFAULT_REPLICATED_AT; @Property(label = "Replicated At", description = "The 'value' used to set the 'replicated at' property. [ Default: Package Last Modified ]", options = { @PropertyOption(name = "PACKAGE_LAST_MODIFIED", value = "Package Last Modified"), @PropertyOption(name = "CURRENT_TIME", value = "Current Time") }) public static final String PROP_REPLICATED_AT = "replicated-at"; private static final String SERVICE_NAME = "package-replication-status-event-listener"; private static final Map<String, Object> AUTH_INFO; static { AUTH_INFO = Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) SERVICE_NAME); } @Override @SuppressWarnings("squid:S3776") public final void handleEvent(final Event event) { if (this.isMaster) { // Only run on master final Map<String, Object> jobConfig = getInfoFromEvent(event); final String[] paths = (String[]) jobConfig.get(PROPERTY_PATHS); if (this.containsJcrPackagePath(paths)) { ResourceResolver resourceResolver = null; try { resourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO); final List<JcrPackage> jcrPackages = this.getJcrPackages(resourceResolver, paths); if (CollectionUtils.isNotEmpty(jcrPackages)) { for (final JcrPackage jcrPackage : jcrPackages) { // Close jcrPackages after they've been used to check if a Job should be invoked. jcrPackage.close(); } jobManager.addJob(JOB_TOPIC, jobConfig); } } catch (LoginException e) { log.error("Could not obtain a resource resolver.", e); } finally { if (resourceResolver != null) { resourceResolver.close(); } } } } } @Override public final JobResult process(final Job job) { final String[] paths = (String[]) job.getProperty(PROPERTY_PATHS); final String replicatedBy = StringUtils.defaultIfEmpty(this.replicatedByOverride, (String) job.getProperty(PROPERTY_REPLICATED_BY)); log.debug("Processing Replication Status Update for JCR Package: {}", paths); ResourceResolver resourceResolver = null; try { resourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO); final List<JcrPackage> jcrPackages = this.getJcrPackages(resourceResolver, paths); if (CollectionUtils.isEmpty(jcrPackages)) { log.warn("JCR Package is unavailable for Replication Status Update at: {}", paths); return JobResult.OK; } for (final JcrPackage jcrPackage : jcrPackages) { try { setReplicationStatus(jcrPackage, replicatedBy, resourceResolver); } finally { // Close each package when we are done. jcrPackage.close(); } } } catch (LoginException e) { logJobError(job, "Could not obtain a resource resolver for applying replication status updates", e); return JobResult.FAILED; } catch (RepositoryException e) { logJobError(job, "Could not update replication metadata", e); return JobResult.FAILED; } finally { if (resourceResolver != null) { resourceResolver.close(); } } return JobResult.OK; } /** * Emits the given error and exception either with level WARN or ERROR depending on whether the job is retried. * This method can be removed once <a href="https://issues.apache.org/jira/browse/SLING-7756">SLING-7756</a> is resolved. * @param job * @param errorMessage * @param e * */ private void logJobError(Job job, String errorMessage, Exception e) { if (job.getRetryCount() < job.getNumberOfRetries()) { log.warn("Job failed with error '{}' in attempt '{}', retry later.", errorMessage, job.getRetryCount(), e); } else { log.error("Job permanently failed with error '{}' in attempt '{}', no more retries", errorMessage, job.getRetryCount(), e); } } private void setReplicationStatus(JcrPackage jcrPackage, String replicatedBy, ResourceResolver resourceResolver) throws RepositoryException { final List<Resource> resources = new ArrayList<>(); final String packageId; try { JcrPackageDefinition packageDefinition = jcrPackage.getDefinition(); if (packageDefinition == null) { throw new RepositoryException( "Could not determine the ID for just replicated package (package invalid?)"); } else { packageId = packageDefinition.getId().toString(); } } catch (RepositoryException e) { throw new RepositoryException( "Could not determine the ID for just replicated package (package invalid?).", e); } try { for (final String packagePath : packageHelper.getContents(jcrPackage)) { final Resource resource = resourceResolver.getResource(packagePath); if (this.accept(resource)) { resources.add(resource); } } } catch (RepositoryException | PackageException | IOException e) { throw new RepositoryException( "Could not retrieve the Packages contents for package '" + packageId + "'", e); } try { if (resources.size() > 0) { replicationStatusManager.setReplicationStatus(resourceResolver, replicatedBy, getJcrPackageLastModified(jcrPackage), ReplicationStatusManager.Status.ACTIVATED, resources.toArray(new Resource[resources.size()])); log.info("Updated Replication Status for JCR Package: {}", packageId); } else { log.info( "Could not find any resources in JCR Package [ {} ] that are candidates to have their Replication Status updated", packageId); } } catch (RepositoryException | IOException e) { // enrich exception with path information (limited to 10 paths only) String paths = resources.stream().map(r -> r.getPath()).limit(10).collect(Collectors.joining(", ")); throw new RepositoryException("Exception occurred updating replication status for contents of package '" + packageId + "' covering paths: '" + paths + ", ...'", e); } } /** * Extracts relevant event information from a Granite Replication Event OR a Day CQ Replication event. * @param event the Osgi Event * @return a Map containing the relevant data points. */ protected final Map<String, Object> getInfoFromEvent(Event event) { final Map<String, Object> eventConfig = new HashMap<>(); final ReplicationEvent replicationEvent = ReplicationEvent.fromEvent(event); if (replicationEvent != null) { // Granite event final ReplicationAction replicationAction = replicationEvent.getReplicationAction(); eventConfig.put(PROPERTY_PATHS, replicationAction.getPaths()); eventConfig.put(PROPERTY_REPLICATED_BY, replicationAction.getUserId()); } else { // CQ event String[] paths = (String[]) event.getProperty(ReplicationAction.PROPERTY_PATHS); if (paths == null) { paths = ArrayUtils.EMPTY_STRING_ARRAY; } String userId = (String) event.getProperty(ReplicationAction.PROPERTY_USER_ID); if (StringUtils.isBlank(userId)) { userId = StringUtils.defaultIfEmpty(this.replicatedByOverride, FALLBACK_REPLICATION_USER_ID); } eventConfig.put(PROPERTY_PATHS, paths); eventConfig.put(PROPERTY_REPLICATED_BY, userId); } return eventConfig; } /** * Checks if any path in the array of paths looks like a Jcr Package path. * * Provides a very fast, String-based, in-memory check to weed out most false positives and avoid * resolving the path to a Jcr Package and ensure it is valid. * * @param paths the array of paths * @return true if at least one path looks like a Jcr Package path */ private boolean containsJcrPackagePath(final String[] paths) { for (final String path : paths) { if (StringUtils.startsWith(path, "/etc/packages/") && StringUtils.endsWith(path, ".zip")) { // At least 1 entry looks like a package return true; } } // Nothing looks like a package... return false; } /** * Resolves paths to Jcr Packages. If any path does not resolve to a valid Jcr Package, it is discarded. * * @param paths the list of paths to resolve to Jcr Packages * @return a list of Jcr Packages that correspond to the provided paths */ private List<JcrPackage> getJcrPackages(final ResourceResolver resourceResolver, final String[] paths) { final List<JcrPackage> packages = new ArrayList<JcrPackage>(); for (final String path : paths) { final Resource eventResource = resourceResolver.getResource(path); JcrPackage jcrPackage = null; try { jcrPackage = packaging.open(eventResource.adaptTo(Node.class), false); if (jcrPackage != null) { packages.add(jcrPackage); } } catch (RepositoryException e) { log.warn("Error checking if the path [ {} ] is a JCR Package.", path); } } return packages; } /** * Checks if the ReplicationStatusManager should make the provides resource w replication status. * * @param resource the return * @return true is the resource is markable resource * @throws RepositoryException */ @SuppressWarnings("squid:S3776") private boolean accept(final Resource resource) throws RepositoryException { if (resource == null || ResourceUtil.isNonExistingResource(resource)) { return false; } for (final Map.Entry<String, Pattern> nodeTypeAndPathRestriction : this.pathRestrictionByNodeType .entrySet()) { final String[] hierarchyNodeTypes = StringUtils.split(nodeTypeAndPathRestriction.getKey(), "/"); boolean match = true; Resource walkingResource = resource; for (int i = (hierarchyNodeTypes.length - 1); i >= 0; i--) { if (walkingResource == null) { match = false; break; } else { final Node node = walkingResource.adaptTo(Node.class); if (node == null || !node.isNodeType(hierarchyNodeTypes[i])) { match = false; break; } walkingResource = walkingResource.getParent(); } } if (match) { // check path restrictions Pattern pathRestriction = nodeTypeAndPathRestriction.getValue(); if (pathRestriction != null && !pathRestriction.matcher(resource.getPath()).matches()) { log.debug( "Path restriction '{}' prevents the resource at '{}' from getting its replication status updated!", pathRestriction, resource.getPath()); return false; } return true; } } return false; } /** * Gets the last build time of the package. * * @param jcrPackage the package obj * @return the package's last build time or null if none can be found * @throws RepositoryException */ private Calendar getJcrPackageLastModified(final JcrPackage jcrPackage) throws RepositoryException, IOException { if (ReplicatedAt.CURRENT_TIME.equals(this.replicatedAt)) { return Calendar.getInstance(); } else { return jcrPackage.getPackage().getCreated(); } } @Activate protected void activate(final Map<String, String> config) throws LoginException { log.trace("Activating the ACS AEM Commons - JCR Package Replication Status Updater (Event Handler)"); this.replicatedByOverride = PropertiesUtil.toString(config.get(PROP_REPLICATED_BY_OVERRIDE), PropertiesUtil .toString(config.get(LEGACY_PROP_REPLICATED_BY_OVERRIDE), DEFAULT_REPLICATED_BY_OVERRIDE)); String tmp = PropertiesUtil.toString(config.get(PROP_REPLICATED_AT), ""); try { this.replicatedAt = ReplicatedAt.valueOf(tmp); } catch (IllegalArgumentException ex) { this.replicatedAt = ReplicatedAt.PACKAGE_LAST_MODIFIED; } final String[] nodeTypeAndPathRestrictions = PropertiesUtil.toStringArray( config.get(PROP_REPLICATION_STATUS_NODE_TYPES), DEFAULT_REPLICATION_STATUS_NODE_TYPES); // the map must keep the order! pathRestrictionByNodeType = new LinkedHashMap<>(); for (String nodeTypeAndPathRestrictionEntry : nodeTypeAndPathRestrictions) { Map.Entry<String, String> nodeTypeAndPathRestriction = ParameterUtil .toMapEntryWithOptionalValue(nodeTypeAndPathRestrictionEntry, " "); final Pattern pathRestrictionPattern; if (StringUtils.isNotBlank(nodeTypeAndPathRestriction.getValue())) { pathRestrictionPattern = Pattern.compile(nodeTypeAndPathRestriction.getValue()); } else { pathRestrictionPattern = null; } pathRestrictionByNodeType.put(nodeTypeAndPathRestriction.getKey(), pathRestrictionPattern); } log.info("Package Replication Status - Replicated By Override User: [ {} ]", this.replicatedByOverride); log.info("Package Replication Status - Replicated At: [ {} ]", this.replicatedAt.toString()); log.info("Package Replication Status - Node Types and Path Restrictions: [ {} ]", pathRestrictionByNodeType); } @Override public final void bindRepository(String repositoryId, String clusterId, boolean newIsMaster) { this.isMaster = newIsMaster; } @Override public final void unbindRepository() { this.isMaster = false; } }