/* * Copyright 2012 david gonzalez. * * 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. */ package com.activecq.samples.replication.impl; import com.day.cq.commons.jcr.JcrConstants; import com.day.cq.replication.Agent; import com.day.cq.replication.AgentFilter; import com.day.cq.replication.AgentManager; 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 org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.SlingConstants; 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.ValueMap; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.sling.event.EventUtil; import org.apache.sling.event.jobs.JobProcessor; import org.apache.sling.event.jobs.JobUtil; import org.osgi.service.component.ComponentContext; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.ValueFormatException; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.VersionException; import java.util.Calendar; import java.util.Date; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; @Component(label="Samples - Reverse Replicator", description="Provides a simple mechanism for reverse replicating nodes. A Reverse replicator agent must be setup on the Server.", configurationFactory=true, immediate=true, metatype=true) @Properties ({ @Property( name="service.vendor", value="ActiveCQ" ), @Property( label="Sling Event Topics", name="event.topics", description="Sling Events to listen to via this Sling Event Handler." + "Values are limited to: " + "org/apache/sling/api/resource/Resource/ADDED, " + "org/apache/sling/api/resource/Resource/CHANGED, " + "org/apache/sling/api/resource/Resource/REMOVED", value={ org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_ADDED, org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_CHANGED, org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_REMOVED } ) }) @Service public class ReverseReplicatorImpl implements JobProcessor, EventHandler { public static final String SLING_TOPIC_ADDED = "org/apache/sling/api/resource/Resource/ADDED"; public static final String SLING_TOPIC_CHANGED = "org/apache/sling/api/resource/Resource/CHANGED"; public static final String SLING_TOPIC_REMOVED = "org/apache/sling/api/resource/Resource/REMOVED"; @Reference AgentManager agentManager; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private Replicator replicator; @Reference private EventAdmin eventAdmin; //private ResourceResolver adminResourceResolver; //private Session adminSession; protected final Logger log = LoggerFactory.getLogger(this.getClass()); private static final boolean DEFAULT_ENABLED = true; private boolean enabled = DEFAULT_ENABLED; @Property(label="Enable", description="Enables this reverse replication configuration.", boolValue=DEFAULT_ENABLED) private static final String PROP_ENABLED = "prop.enabled"; private static final boolean DEFAULT_SYNCHRONOUS = false; private boolean sychronous = DEFAULT_SYNCHRONOUS; @Property(label="Synchronous Replication", description="Should the replication be done synchronous or asynchronous? The default is 'false'.", boolValue=DEFAULT_SYNCHRONOUS) private static final String PROP_SYNCHRONOUS = "prop.synchronous"; private static final boolean DEFAULT_SUPRESS_STATUS_UPDATE = true; private boolean suppressStatusUpdate = DEFAULT_SUPRESS_STATUS_UPDATE; @Property(label="Supress Status Update", description="If set to true the replication will not update the replication status properties after a replication. Default is 'true'.", boolValue=DEFAULT_SUPRESS_STATUS_UPDATE) private static final String PROP_SUPRESS_STATUS_UPDATE = "prop.supress-status-update"; private static final boolean DEFAULT_SUPRESS_VERSIONING = true; private boolean supressVersioning = DEFAULT_SUPRESS_VERSIONING; @Property(label="Supress Versioning", description="If set to true the replication will not trigger implicit versioning. Default is 'true'", boolValue=DEFAULT_SUPRESS_VERSIONING) private static final String PROP_SUPRESS_VERSIONING = "prop.supress-versioning"; private static final String[] DEFAULT_PATHS = { "/path/to/replicate" }; private String[] paths = DEFAULT_PATHS; @Property(label="Paths to replicate", description="JCR paths to listen on.", cardinality=Integer.MAX_VALUE, value={"/path/to/replicate"}) private static final String PROP_PATHS = "prop.paths"; private static final String[] DEFAULT_PRIMARY_TYPES = { }; private String[] primaryTypes = DEFAULT_PRIMARY_TYPES; @Property(label="Primary Node Types", description="jcr:primaryType's to reverse replciate. Leave blank to disable this filter.", cardinality=Integer.MAX_VALUE, value={""}) private static final String PROP_PRIMARY_TYPES = "prop.primary-types"; private static final String[] DEFAULT_PROPERTY_MATCHES = { "cq:distribute=true" }; private Map<String, String> propertyMatches = new HashMap<String, String>(); @Property(label="Primary Node Types to Replicate.", description="Format is: <property>=<value>. Leave blank to disable this filter.", cardinality=Integer.MAX_VALUE, value={"cq:distribute=true"}) private static final String PROP_PROPERTY_MATCHES = "prop.property-matches"; private static final String[] DEFAULT_PATH_BLACKLIST = { }; private String[] pathBlacklist = DEFAULT_PATH_BLACKLIST; @Property(label="Blacklist Regex", description="", cardinality=Integer.MAX_VALUE, value={}) private static final String PROP_PATH_BLACKLIST = "prop.path-blacklist"; private static final String[] DEFAULT_PATH_WHITELIST = { }; private String[] pathWhitelist = DEFAULT_PATH_WHITELIST; @Property(label="Whitelist Regex", description="", cardinality=Integer.MAX_VALUE, value={}) private static final String PROP_PATH_WHITELIST = "prop.path-whitelist"; /** * Sling Event Handling * * handleEvent and process implement the Sling Eventing which handles * syncing changes from the JCR/CRX to the FileSystem via: - VAULT UPDATE * */ public void handleEvent(Event event) { if (EventUtil.isLocal(event)) { JobUtil.processJob(event, this); } } public boolean process(Event event) { if(!enabled) { return false; } ResourceResolver adminResourceResolver = null; try { adminResourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null); final String resourcePath = (String) event.getProperty("path"); for (final String path : paths) { if (StringUtils.startsWithIgnoreCase(resourcePath, path)) { log.debug("Processing Reverse Replication Event for: " + resourcePath); Resource resource = adminResourceResolver.resolve(resourcePath); if(resource == null) { continue; } log.debug("Primary Type: " + hasValidPrimaryType(resource)); log.debug("Property: " + hasValidProperty(resource)); log.debug("Whitelist: " + isWhitelisted(resource)); log.debug("Not Blacklist: " + !isBlacklisted(resource)); log.debug("Event: " + event.getTopic()); log.debug("is Delete: " + isDeleteEvent(event)); if(!isDeleteEvent(event)) { if(!hasValidPrimaryType(resource) || !hasValidProperty(resource)) { continue; } } if(!isWhitelisted(resource) || isBlacklisted(resource)) { continue; } if(!shouldReplicate(resource, event)) { continue; } try { replicate(resource, event); log.debug("*** REPLICATION KICKED OFF ***"); break; } catch (ReplicationException ex) { log.debug("*** REPLICATION FAILED ***"); log.debug(ex.getMessage()); } } } } catch(Exception ex) { log.debug("*** REPLICATION FAILED : REPOSITORY EXCEPTION GETTING ADMIN RESOURCE RESOLVER ***"); log.debug(ex.getMessage()); } finally { if (adminResourceResolver != null) { adminResourceResolver.close(); } } return true; } private boolean shouldReplicate(final Resource resource, Event event) { if(StringUtils.equals(event.getTopic(), SlingConstants.TOPIC_RESOURCE_CHANGED)) { ValueMap properties = resource.adaptTo(ValueMap.class); final Date lastModified = properties.get(JcrConstants.JCR_LASTMODIFIED, Date.class); final Date lastReplicated = properties.get("cq:lastReplicated", Date.class); if(lastReplicated == null) { return true; } if(lastModified == null) { setLastModified(resource); } log.debug("LM " + lastModified.getTime() + " >= LR " + lastReplicated.getTime() + " => " + lastModified.after(lastReplicated)); // Last Modified must be >= Last Replicated return lastModified.after(lastReplicated); } else { return true; } } private void setLastModified(final Resource resource) { try { Calendar now = Calendar.getInstance(); Node node = resource.adaptTo(Node.class); node.setProperty(JcrConstants.JCR_LASTMODIFIED, now); node.setProperty(JcrConstants.JCR_LAST_MODIFIED_BY, resource.getResourceResolver().getUserID()); node.getSession().save(); } catch (ValueFormatException ex) { java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (VersionException ex) { java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (LockException ex) { java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (ConstraintViolationException ex) { java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (RepositoryException ex) { java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex); } } private boolean isWhitelisted(final Resource resource) { if(pathWhitelist.length <= 0) { return true; } for(String regex : pathWhitelist) { if(StringUtils.stripToNull(regex) == null) { continue; } log.debug("White : " + regex + " vs " + resource.getPath()); Pattern p = Pattern.compile(regex); Matcher m = p.matcher(resource.getPath()); if(m.find()) { return true; } } return false; } private boolean isBlacklisted(Resource resource) { if(pathBlacklist.length <= 0) { return false; } for(String regex : pathBlacklist) { if(StringUtils.stripToNull(regex) == null) { continue; } Pattern p = Pattern.compile(regex); Matcher m = p.matcher(resource.getPath()); if(m.find()) { return true; } } return false; } private boolean hasValidPrimaryType(Resource resource) { if(primaryTypes.length <= 0) { return true; } ValueMap properties = resource.adaptTo(ValueMap.class); for(final String primaryType : primaryTypes) { if(StringUtils.stripToNull(primaryType) == null) { continue; } String tmp = properties.get(JcrConstants.JCR_PRIMARYTYPE, String.class); log.debug("PT : " + tmp + " vs " + primaryType); if(StringUtils.equals(primaryType, tmp)) { return true; } } return false; } private boolean hasValidProperty(Resource resource) { if(propertyMatches.isEmpty()) { return true; } // Removal event if(resource.adaptTo(Node.class) == null) { return true; } ValueMap properties = resource.adaptTo(ValueMap.class); for(final String property : propertyMatches.keySet()) { if(StringUtils.stripToNull(property) == null) { continue; } final String value = (String)propertyMatches.get(property); if(properties.containsKey(property)) { String tmp = properties.get(property, String.class); if(StringUtils.equals(value, tmp)) { log.debug("Expected(" + property + ": " + value + ") -> Actual(" + property + ": " + tmp + ")"); return true; } } } return false; } private void replicate(final Resource resource, final Event event) throws ReplicationException { if(resource == null) { return; } final ReplicationOptions replicationOptions = new ReplicationOptions(); final Session adminSession = resource.getResourceResolver().adaptTo(Session.class); final String revision = (String) resource.getResourceMetadata().get("resourceVersion"); if(revision != null) { replicationOptions.setRevision(revision); } replicationOptions.setFilter(DISTRIBUTE_AGENT_FILTER); replicationOptions.setSynchronous(sychronous); replicationOptions.setSuppressStatusUpdate(suppressStatusUpdate); replicationOptions.setSuppressVersions(supressVersioning); if(canReplicate(null, resource.getPath())) {//adminResourceResolver.adaptTo(User.class), resource.getPath())) { replicator.replicate(adminSession, getReplicationActionType(event), resource.getPath(), replicationOptions); } else { final String path = resource.getPath(); log.error((new StringBuilder()).append(adminSession.getUserID()).append(" is not allowed to replicate this page/asset ").append(path).append(". Issuing request for 'replication'").toString()); final Dictionary properties = new Hashtable<String, Object>(); properties.put("path", path); properties.put("replicationType", getReplicationActionType(event)); final Event activationEvent = new Event("com/day/cq/wcm/workflow/req/for/activation", properties); eventAdmin.sendEvent(activationEvent); } } protected boolean canReplicate(Object user, String path) { // Uses Admin Session; Can always replicat. return true; // return user.hasPermissionOn("wcm/core/privileges/replicate", path); } protected ReplicationActionType getReplicationActionType(Event event) { if(isDeleteEvent(event)) { return ReplicationActionType.DELETE; } else { return ReplicationActionType.ACTIVATE; } } protected boolean isDeleteEvent(Event event) { return SlingConstants.TOPIC_RESOURCE_REMOVED.toString().equals( event.getTopic()) || SLING_TOPIC_REMOVED.equals(event.getTopic()); } protected void activate(ComponentContext componentContext) { Dictionary properties = componentContext.getProperties(); enabled = PropertiesUtil.toBoolean(properties.get(PROP_ENABLED), DEFAULT_ENABLED); log.debug("Enabled: " + enabled); sychronous = PropertiesUtil.toBoolean(properties.get(PROP_SYNCHRONOUS), DEFAULT_SYNCHRONOUS); suppressStatusUpdate = PropertiesUtil.toBoolean(properties.get(PROP_SUPRESS_STATUS_UPDATE), DEFAULT_SUPRESS_STATUS_UPDATE); supressVersioning = PropertiesUtil.toBoolean(properties.get(PROP_SUPRESS_VERSIONING), DEFAULT_SUPRESS_VERSIONING); paths = PropertiesUtil.toStringArray(properties.get(PROP_PATHS), DEFAULT_PATHS); paths = (String[]) ArrayUtils.removeElement(paths, ""); pathWhitelist = PropertiesUtil.toStringArray(properties.get(PROP_PATH_WHITELIST), DEFAULT_PATH_WHITELIST); pathWhitelist = (String[]) ArrayUtils.removeElement(pathWhitelist, ""); pathBlacklist = PropertiesUtil.toStringArray(properties.get(PROP_PATH_BLACKLIST), DEFAULT_PATH_BLACKLIST); pathBlacklist = (String[]) ArrayUtils.removeElement(pathBlacklist, ""); primaryTypes = PropertiesUtil.toStringArray(properties.get(PROP_PRIMARY_TYPES), DEFAULT_PRIMARY_TYPES); primaryTypes = (String[]) ArrayUtils.removeElement(primaryTypes, ""); String[] tmp = PropertiesUtil.toStringArray(properties.get(PROP_PROPERTY_MATCHES), DEFAULT_PROPERTY_MATCHES); tmp = (String[]) ArrayUtils.removeElement(tmp, ""); for(final String t : tmp) { String[] s = StringUtils.split(t, '='); if(s == null || s.length != 2) { continue; } propertyMatches.put(s[0], s[1]); } } protected void deactivate(ComponentContext componentContext) { } private static final AgentFilter DISTRIBUTE_AGENT_FILTER = new AgentFilter() { public boolean isIncluded(Agent agent) { return agent.getConfiguration().isTriggeredOnDistribute(); } }; }