/*
* 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.infinispan.commons.util.ReflectionUtil;
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.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;
this.components = config.getSequencing().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 {
String threadPoolName = config.getSequencing().getThreadPoolName();
this.sequencingExecutor = repository.context().getCachedTreadPool(threadPoolName);
this.workQueue = new SequencingWorkQueue();
this.processId = repository.context().getProcessId();
ExecutionContext context = this.repository.context();
this.stringFactory = context.getValueFactories().getStringFactory();
this.sequencersById = new HashMap<UUID, Sequencer>();
this.sequencersByName = new HashMap<String, Sequencer>();
this.configByWorkspaceName = new HashMap<String, Collection<SequencingConfiguration>>();
this.pathExpressionsBySequencerId = new HashMap<UUID, Collection<SequencerPathExpression>>();
String repoName = repository.name();
for (Component component : components) {
try {
Sequencer sequencer = component.createInstance(getClass().getClassLoader());
// Set the repository name field ...
ReflectionUtil.setValue(sequencer, "repositoryName", repoName);
// Set the logger instance
ReflectionUtil.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 = ReflectionUtil.findMethod(Sequencer.class, "postInitialize");
ReflectionUtil.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 + "'") : "");
}
}
}