/* * ModeShape (http://www.modeshape.org) * * 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 org.modeshape.jcr; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.jcr.NamespaceRegistry; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NodeTypeManager; import org.modeshape.common.SystemFailureException; import org.modeshape.common.annotation.Immutable; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.HashCode; import org.modeshape.common.util.Reflection; import org.modeshape.jcr.RepositoryConfiguration.Component; import org.modeshape.jcr.api.monitor.ValueMetric; import org.modeshape.jcr.api.sequencer.Sequencer; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.change.Change; import org.modeshape.jcr.cache.change.ChangeSet; import org.modeshape.jcr.cache.change.ChangeSetListener; import org.modeshape.jcr.cache.change.PropertyAdded; import org.modeshape.jcr.cache.change.PropertyChanged; import org.modeshape.jcr.cache.change.WorkspaceAdded; import org.modeshape.jcr.cache.change.WorkspaceRemoved; import org.modeshape.jcr.sequencer.InvalidSequencerPathExpression; import org.modeshape.jcr.sequencer.SequencerPathExpression; import org.modeshape.jcr.sequencer.SequencerPathExpression.Matcher; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.ValueFactory; /** * Component that manages the library of sequencers configured for a repository. Simply instantiate, and register as a * {@link ChangeSetListener listener} of cache changes. * <p> * This class keeps a cache of the {@link SequencerPathExpression} instances (and the corresponding {@link Sequencer} * implementation) for each workspace. This is so that it's much easier and more efficient to process the events, which happens * very frequently. Also, that structure is a bit backward compared to how the sequencers are defined in the configuration. * </p> */ @Immutable public class Sequencers implements ChangeSetListener { /** We don't use the standard logging convention here; we want clients to easily configure logging for sequencing */ private static final Logger LOGGER = Logger.getLogger("org.modeshape.jcr.sequencing.sequencers"); private static final boolean TRACE = LOGGER.isTraceEnabled(); private static final boolean DEBUG = LOGGER.isDebugEnabled(); protected final JcrRepository.RunningState repository; private final Map<UUID, Sequencer> sequencersById; private final Map<String, Sequencer> sequencersByName; private final Collection<Component> components; private final Lock configChangeLock = new ReentrantLock(); private final Map<UUID, Collection<SequencerPathExpression>> pathExpressionsBySequencerId; private volatile Map<String, Collection<SequencingConfiguration>> configByWorkspaceName; private final String systemWorkspaceKey; private final String processId; private final ValueFactory<String> stringFactory; private final WorkQueue workQueue; protected final ExecutorService sequencingExecutor; private boolean initialized; private volatile boolean acceptsWork = true; protected Sequencers( JcrRepository.RunningState repository, RepositoryConfiguration config, Iterable<String> workspaceNames ) { this.repository = repository; RepositoryConfiguration.Sequencing sequencing = config.getSequencing(); this.components = sequencing.getSequencers(repository.problems()); this.systemWorkspaceKey = repository.repositoryCache().getSystemKey().getWorkspaceKey(); if (components.isEmpty()) { this.processId = null; this.stringFactory = null; this.configByWorkspaceName = null; this.sequencersById = null; this.pathExpressionsBySequencerId = null; this.sequencingExecutor = null; this.workQueue = null; this.initialized = true; this.sequencersByName = Collections.emptyMap(); } else { int maxThreadCount = sequencing.getMaxPoolSize(); String threadPoolName = sequencing.getThreadPoolName(); this.sequencingExecutor = repository.context().getCachedTreadPool(threadPoolName, maxThreadCount); this.workQueue = new SequencingWorkQueue(); this.processId = repository.context().getProcessId(); ExecutionContext context = this.repository.context(); this.stringFactory = context.getValueFactories().getStringFactory(); this.sequencersById = new HashMap<>(); this.sequencersByName = new HashMap<>(); this.configByWorkspaceName = new HashMap<>(); this.pathExpressionsBySequencerId = new HashMap<>(); String repoName = repository.name(); for (Component component : components) { try { Sequencer sequencer = component.createInstance(); // Set the repository name field ... Reflection.setValue(sequencer, "repositoryName", repoName); // Set the logger instance Reflection.setValue(sequencer, "logger", ExtensionLogger.getLogger(sequencer.getClass())); // We'll initialize it later in #intialize() ... sequencersByName.put(sequencer.getName(), sequencer); if (sequencer.getPathExpressions().length == 0) { // There are no path expressions, so this sequencer is only for explicit invocation ... if (DEBUG) { LOGGER.debug("Created sequencer '{0}' in repository '{1}' with no path expression; availabe only for explicit invocation", sequencer.getName(), repository.name()); } } else { // For each sequencer, figure out which workspaces apply ... UUID uuid = sequencer.getUniqueId(); sequencersById.put(sequencer.getUniqueId(), sequencer); // For each sequencer, create the path expressions ... Set<SequencerPathExpression> pathExpressions = buildPathExpressionSet(sequencer); pathExpressionsBySequencerId.put(uuid, pathExpressions); if (DEBUG) { LOGGER.debug("Created sequencer '{0}' in repository '{1}' with valid path expressions: {2}", sequencer.getName(), repository.name(), pathExpressions); } } } catch (Throwable t) { if (t.getCause() != null) { t = t.getCause(); } this.repository.error(t, JcrI18n.unableToInitializeSequencer, component, repoName, t.getMessage()); } } // Now process each workspace ... for (String workspaceName : workspaceNames) { workspaceAdded(workspaceName); } repository.changeBus().register(this); this.initialized = false; } } private Sequencers( Sequencers original, JcrRepository.RunningState repository ) { this.repository = repository; this.sequencingExecutor = original.sequencingExecutor; this.workQueue = original.workQueue; this.systemWorkspaceKey = original.systemWorkspaceKey; this.processId = original.processId; this.stringFactory = repository.context().getValueFactories().getStringFactory(); this.components = original.components; this.sequencersById = original.sequencersById; this.sequencersByName = original.sequencersByName; this.configByWorkspaceName = original.configByWorkspaceName; this.pathExpressionsBySequencerId = original.pathExpressionsBySequencerId; } protected Sequencers with( JcrRepository.RunningState repository ) { return repository == this.repository ? this : new Sequencers(this, repository); } protected void initialize() { if (initialized) { // nothing to do ... return; } // Get a session that we'll pass to the sequencers to use for registering namespaces and node types Session session = null; List<Sequencer> initialized = new ArrayList<>(); try { // Get a session that we'll pass to the sequencers to use for registering namespaces and node types session = repository.loginInternalSession(); NamespaceRegistry registry = session.getWorkspace().getNamespaceRegistry(); NodeTypeManager nodeTypeManager = session.getWorkspace().getNodeTypeManager(); if (!(nodeTypeManager instanceof org.modeshape.jcr.api.nodetype.NodeTypeManager)) { throw new IllegalStateException("Invalid node type manager (expected modeshape NodeTypeManager): " + nodeTypeManager.getClass().getName()); } // Initialize each sequencer using the supplied session ... for (Iterator<Map.Entry<String, Sequencer>> sequencersIterator = sequencersByName.entrySet().iterator(); sequencersIterator.hasNext();) { Sequencer sequencer = sequencersIterator.next().getValue(); try { sequencer.initialize(registry, (org.modeshape.jcr.api.nodetype.NodeTypeManager)nodeTypeManager); // If successful, call the 'postInitialize' method reflectively (due to inability to call directly) ... Method postInitialize = Reflection.findMethod(Sequencer.class, "postInitialize"); Reflection.invokeAccessibly(sequencer, postInitialize, new Object[] {}); if (DEBUG) { LOGGER.debug("Successfully initialized sequencer '{0}' in repository '{1}'", sequencer.getName(), repository.name()); } initialized.add(sequencer); } catch (Throwable t) { if (t.getCause() != null) { t = t.getCause(); } repository.error(t, JcrI18n.unableToInitializeSequencer, sequencer, repository.name(), t.getMessage()); try { sequencersIterator.remove(); } finally { sequencersById.remove(sequencer.getUniqueId()); } } } this.initialized = true; } catch (RepositoryException e) { throw new SystemFailureException(e); } finally { if (session != null) { session.logout(); } } assert allSequencersInitialized(initialized); } private boolean allSequencersInitialized( Collection<Sequencer> initialized ) { assert initialized.size() == sequencersByName.size(); for (Sequencer sequencer : sequencersByName.values()) { if (!initialized.contains(sequencer)) return false; } return true; } /** * Determine if there are no sequencers configured. * * @return true if there are no sequencers, or false if there is at least one. */ public boolean isEmpty() { return this.components.size() == 0; } /** * Signal that a new workspace was added. * * @param workspaceName the workspace name; may not be null */ protected void workspaceAdded( String workspaceName ) { String workspaceKey = NodeKey.keyForWorkspaceName(workspaceName); if (systemWorkspaceKey.equals(workspaceKey)) { // No sequencers for the system workspace! return; } Collection<SequencingConfiguration> configs = new LinkedList<SequencingConfiguration>(); // Go through the sequencers to see which apply to this workspace ... for (Sequencer sequencer : sequencersById.values()) { boolean updated = false; for (SequencerPathExpression expression : pathExpressionsBySequencerId.get(sequencer.getUniqueId())) { if (expression.appliesToWorkspace(workspaceName)) { updated = true; configs.add(new SequencingConfiguration(expression, sequencer)); } } if (DEBUG && updated) { LOGGER.debug("Updated sequencer '{0}' (id={1}) configuration due to new workspace '{2}' in repository '{3}'", sequencer.getName(), sequencer.getUniqueId(), workspaceName, repository.name()); } } if (configs.isEmpty()) return; // Otherwise, update the configs by workspace key ... try { configChangeLock.lock(); // Make a copy of the existing map ... Map<String, Collection<SequencingConfiguration>> configByWorkspaceName = new HashMap<String, Collection<SequencingConfiguration>>( this.configByWorkspaceName); // Insert the new information ... configByWorkspaceName.put(workspaceName, configs); // Replace the exisiting map (which is used without a lock) ... this.configByWorkspaceName = configByWorkspaceName; } finally { configChangeLock.unlock(); } } /** * Signal that a new workspace was removed. * * @param workspaceName the workspace name; may not be null */ protected void workspaceRemoved( String workspaceName ) { // Otherwise, update the configs by workspace key ... try { configChangeLock.lock(); // Make a copy of the existing map ... Map<String, Collection<SequencingConfiguration>> configByWorkspaceName = new HashMap<String, Collection<SequencingConfiguration>>( this.configByWorkspaceName); // Insert the new information ... if (configByWorkspaceName.remove(workspaceName) != null) { // Replace the exisiting map (which is used without a lock) ... this.configByWorkspaceName = configByWorkspaceName; } } finally { configChangeLock.unlock(); } } /** * @return stringFactory */ protected final ValueFactory<String> stringFactory() { return stringFactory; } protected final void shutdown() { // mark it as shutdown first, before attempting to terminate any existing jobs acceptsWork = false; if (workQueue != null) { sequencingExecutor.shutdown(); workQueue.shutdown(); } } protected final RepositoryStatistics statistics() { return repository.statistics(); } protected void submitWork( SequencingConfiguration sequencingConfig, Matcher matcher, String inputWorkspaceName, String propertyName, String userId ) { if (!acceptsWork) return; // Convert the input path (which has a '@' to denote a property) to a standard JCR path ... SequencingWorkItem workItem = new SequencingWorkItem(sequencingConfig.getSequencer().getUniqueId(), userId, inputWorkspaceName, matcher.getSelectedPath(), matcher.getJcrInputPath(), matcher.getOutputPath(), matcher.getOutputWorkspaceName(), propertyName); statistics().increment(ValueMetric.SEQUENCER_QUEUE_SIZE); workQueue.submit(workItem); } protected Sequencer getSequencer( UUID id ) { return sequencersById.get(id); } public Sequencer getSequencer( String sequencerName ) { return sequencersByName.get(sequencerName); } protected Set<SequencerPathExpression> buildPathExpressionSet( Sequencer sequencer ) throws InvalidSequencerPathExpression { String[] pathExpressions = sequencer.getPathExpressions(); if (pathExpressions.length == 0) { String msg = RepositoryI18n.atLeastOneSequencerPathExpressionMustBeSpecified.text(repository.name(), sequencer.getName()); throw new InvalidSequencerPathExpression(msg); } // Compile the path expressions ... Set<SequencerPathExpression> result = new LinkedHashSet<SequencerPathExpression>(); for (String pathExpression : pathExpressions) { assert pathExpression != null; assert pathExpression.length() != 0; SequencerPathExpression expression = SequencerPathExpression.compile(pathExpression); result.add(expression); } return Collections.unmodifiableSet(result); } protected boolean acceptsWork() { return acceptsWork; } @Immutable protected static final class SequencingContext implements Sequencer.Context { private final DateTime now; private final org.modeshape.jcr.api.ValueFactory valueFactory; protected SequencingContext( DateTime now, org.modeshape.jcr.api.ValueFactory jcrValueFactory ) { this.now = now; this.valueFactory = jcrValueFactory; } @Override public Calendar getTimestamp() { return now.toCalendar(); } @Override public org.modeshape.jcr.api.ValueFactory valueFactory() { return valueFactory; } } /** * This method is called when changes are persisted to the repository. This method quickly looks at the changes and decides * which (if any) sequencers should be called, and enqueues any sequencing work in the supplied work queue for subsequent * asynchronous processing. * * @param changeSet the changes */ @Override public void notify( ChangeSet changeSet ) { if (this.processId == null) { // No sequencers, so return immediately ... return; } if (!processId.equals(changeSet.getProcessKey())) { // We didn't generate these changes, so skip them ... return; } final String workspaceName = changeSet.getWorkspaceName(); final Collection<SequencingConfiguration> configs = this.configByWorkspaceName.get(workspaceName); if (configs == null) { // No sequencers apply to this workspace ... return; } try { // Now process the changes ... for (Change change : changeSet) { // Look at property added and removed events. if (change instanceof PropertyAdded) { PropertyAdded added = (PropertyAdded)change; Path nodePath = added.getPathToNode(); String strPath = stringFactory.create(nodePath); Name propName = added.getProperty().getName(); // Check if the property is sequencable ... for (SequencingConfiguration config : configs) { Matcher matcher = config.matches(strPath, propName); if (!matcher.matches()) { if (TRACE) { LOGGER.trace("Added property '{1}:{0}' in repository '{2}' did not match sequencer '{3}' and path expression '{4}'", added.getPath(), workspaceName, repository.name(), config.getSequencer().getName(), config.getPathExpression()); } continue; } if (TRACE) { LOGGER.trace("Submitting added property '{1}:{0}' in repository '{2}' for sequencing using '{3}' and path expression '{4}'", added.getPath(), workspaceName, repository.name(), config.getSequencer().getName(), config.getPathExpression()); } // The property should be sequenced ... submitWork(config, matcher, workspaceName, stringFactory.create(propName), changeSet.getUserId()); } } else if (change instanceof PropertyChanged) { PropertyChanged changed = (PropertyChanged)change; Path nodePath = changed.getPathToNode(); String strPath = stringFactory.create(nodePath); Name propName = changed.getNewProperty().getName(); // Check if the property is sequencable ... for (SequencingConfiguration config : configs) { Matcher matcher = config.matches(strPath, propName); if (!matcher.matches()) { if (TRACE) { LOGGER.trace("Changed property '{1}:{0}' in repository '{2}' did not match sequencer '{3}' and path expression '{4}'", changed.getPath(), workspaceName, repository.name(), config.getSequencer().getName(), config.getPathExpression()); } continue; } if (TRACE) { LOGGER.trace("Submitting changed property '{1}:{0}' in repository '{2}' for sequencing using '{3}' and path expression '{4}'", changed.getPath(), workspaceName, repository.name(), config.getSequencer().getName(), config.getPathExpression()); } // The property should be sequenced ... submitWork(config, matcher, workspaceName, stringFactory.create(propName), changeSet.getUserId()); } } // It's possible we should also be looking at other types of events (like property removed or // node added/changed/removed events), but this is consistent with the 2.x behavior. // Handle the workspace changes ... else if (change instanceof WorkspaceAdded) { WorkspaceAdded added = (WorkspaceAdded)change; workspaceAdded(added.getWorkspaceName()); } else if (change instanceof WorkspaceRemoved) { WorkspaceRemoved removed = (WorkspaceRemoved)change; workspaceRemoved(removed.getWorkspaceName()); } } } catch (Throwable e) { LOGGER.error(e, JcrI18n.errorCleaningUpLocks, repository.name()); } } protected static interface WorkQueue { void submit( SequencingWorkItem work ); void shutdown(); } protected final class SequencingWorkQueue implements WorkQueue { private final List<Future<?>> results = new ArrayList<Future<?>>(); @Override public void submit( SequencingWorkItem work ) { results.add(sequencingExecutor.submit(new SequencingRunner(repository, work))); } @Override public void shutdown() { for (Future<?> workItem : results) { workItem.cancel(true); } results.clear(); } } /** * This class represents a single {@link SequencerPathExpression} and the corresponding {@link Sequencer} implementation that * should be used if the path expression matches. */ protected final class SequencingConfiguration { private final Sequencer sequencer; private final SequencerPathExpression pathExpression; protected SequencingConfiguration( SequencerPathExpression expression, Sequencer sequencer ) { this.sequencer = sequencer; this.pathExpression = expression; } /** * @return pathExpression */ public SequencerPathExpression getPathExpression() { return pathExpression; } /** * @return sequencer */ public Sequencer getSequencer() { return sequencer; } /** * Determine if this sequencer configuration matches the supplied changed node and property, meaning the changes should be * sequenced by this sequencer. * * @param pathToChangedNode the path to the added/changed node; may not be null * @param changedPropertyName the name of the changed property; may not be null * @return the matcher that defines whether there's a match, and if so where the sequenced output is to be */ public Matcher matches( String pathToChangedNode, Name changedPropertyName ) { // Put the path in the right form ... String absolutePath = pathToChangedNode + "/@" + stringFactory().create(changedPropertyName); return pathExpression.matcher(absolutePath); } } @Immutable public static final class SequencingWorkItem implements Serializable { private static final long serialVersionUID = 1L; private final UUID sequencerId; private final String inputWorkspaceName; private final String selectedPath; private final String inputPath; private final String changedPropertyName; private final String outputPath; private final String outputWorkspaceName; private final int hc; private final String userId; protected SequencingWorkItem( UUID sequencerId, String userId, String inputWorkspaceName, String selectedPath, String inputPath, String outputPath, String outputWorkspaceName, String changedPropertyName ) { this.userId = userId; this.sequencerId = sequencerId; this.inputWorkspaceName = inputWorkspaceName; this.selectedPath = selectedPath; this.inputPath = inputPath; this.outputPath = outputPath; this.outputWorkspaceName = outputWorkspaceName; this.changedPropertyName = changedPropertyName; this.hc = HashCode.compute(this.sequencerId, this.inputWorkspaceName, this.inputPath, this.changedPropertyName, this.outputPath, this.outputWorkspaceName); assert this.sequencerId != null; assert this.inputPath != null; assert this.changedPropertyName != null; assert this.outputPath != null; } /** * Get the identifier of the sequencer. * * @return the sequencer ID; never null */ public UUID getSequencerId() { return sequencerId; } /** * Get the id (username) of the user which triggered the sequencing * * @return the user id, never null */ public String getUserId() { return userId; } /** * Get the name of the workspace where the input exists. * * @return the input workspace name; never null */ public String getInputWorkspaceName() { return inputWorkspaceName; } /** * Get the input path of the node/property that is to be sequenced. * * @return the input path; never null */ public String getInputPath() { return inputPath; } /** * Get the path of the selected node that is to be sequenced. * * @return the selected path; never null */ public String getSelectedPath() { return selectedPath; } /** * Get the name of the changed property. * * @return the name of the property that was changed; never null */ public String getChangedPropertyName() { return changedPropertyName; } /** * Get the path for the sequencing output. * * @return the output path; never null */ public String getOutputPath() { return outputPath; } /** * Get the name of the workspace where the output is to be written. * * @return the output workspace name; may be null if the output is to be written to the same workspace as the input */ public String getOutputWorkspaceName() { return outputWorkspaceName; } @Override public int hashCode() { return this.hc; } @Override public boolean equals( Object obj ) { if (obj == this) return true; if (obj instanceof SequencingWorkItem) { SequencingWorkItem that = (SequencingWorkItem)obj; if (this.hc != that.hc) return false; if (!this.sequencerId.equals(that.sequencerId)) return false; if (!this.inputWorkspaceName.equals(that.inputWorkspaceName)) return false; if (!this.inputPath.equals(that.inputPath)) return false; if (!this.outputPath.equals(that.outputWorkspaceName)) return false; if (!this.outputWorkspaceName.equals(that.outputWorkspaceName)) return false; return true; } return false; } @Override public String toString() { return sequencerId + " @ " + inputPath + " -> " + outputPath + (outputWorkspaceName != null ? (" in workspace '" + outputWorkspaceName + "'") : ""); } } }