/*
* 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.spi.index.provider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.RepositoryException;
import javax.jcr.query.qom.Constraint;
import javax.jcr.query.qom.JoinCondition;
import org.modeshape.common.annotation.GuardedBy;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.collection.DelegateIterable;
import org.modeshape.common.collection.Problems;
import org.modeshape.common.function.Function;
import org.modeshape.common.function.Predicate;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.Environment;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.ModeShapeLexicon;
import org.modeshape.jcr.NodeTypes;
import org.modeshape.jcr.api.Logger;
import org.modeshape.jcr.api.index.IndexColumnDefinition;
import org.modeshape.jcr.api.index.IndexDefinition;
import org.modeshape.jcr.api.index.IndexDefinition.IndexKind;
import org.modeshape.jcr.bus.ChangeBus;
import org.modeshape.jcr.cache.CachedNode.Properties;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.change.ChangeSet;
import org.modeshape.jcr.cache.change.ChangeSetAdapter.NodeTypePredicate;
import org.modeshape.jcr.cache.change.ChangeSetListener;
import org.modeshape.jcr.cache.change.Observable;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.engine.NoOpQueryIndexWriter;
import org.modeshape.jcr.spi.index.Index;
import org.modeshape.jcr.spi.index.IndexConstraints;
import org.modeshape.jcr.spi.index.IndexCostCalculator;
import org.modeshape.jcr.spi.index.IndexDefinitionChanges;
import org.modeshape.jcr.spi.index.IndexFeedback;
import org.modeshape.jcr.spi.index.IndexManager;
import org.modeshape.jcr.spi.index.IndexWriter;
import org.modeshape.jcr.spi.index.WorkspaceChanges;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PropertyType;
import org.modeshape.jcr.value.ValueFactories;
/**
* A component that provides access to and manages a set of {@link Index indexes} that are described/defined with
* {@link IndexDefinition} objects via the session's {@link IndexManager}. This provider uses each {@link IndexDefinition} to
* define and manage the {@link Index}es for each workspace identified by the definition. Much of this management is the same for
* all providers, so this implementation provides much of that functionality. What each custom provider must do, then, is respond
* to the {@link #createIndex}, {@link #updateIndex}, {@link #removeIndex} methods and return an appropriate {@link ManagedIndex}
* object that behaves as described by the method parameters. While the {@link Index} represents the functionality needed by the
* query engine, the {@link ManagedIndex} is the lowest-level and simplest interface to encapsulate the behavior of a single index
* managed by this provider. for each Only basic functionality is provided, and subclasses specialize the behavior by overriding
* several abstract methods.
* <p>
* Upon startup, the repository examines its configuration and sets up each of the index providers using this sequence:
* <ol>
* <li>Instantiation of a providers via no-arg constructor</li>
* <li>Reflectively set each of the member fields of the provider instance, based upon the provider configuration's properties</li>
* <li>Call the {@link #initialize()} method. This method is final and delegates to {@link #doInitialize()}, which can be
* specialized for any implementation-specific startup behavior.</li>
* <li>Notify this provider of all of its {@link IndexDefinition}s, whether they are defined in the configuration or persisted in
* the repository's system area, by calling
* {@link #notify(IndexDefinitionChanges, ChangeBus, org.modeshape.jcr.NodeTypes.Supplier, Set, IndexFeedback)}. This method
* processes all of the definition changes, and based upon the available workspaces, calls the {@link #createIndex},
* {@link #updateIndex}, and {@link #removeIndex} methods for each index/workspace combination. Each of these methods can request
* via the {@link IndexFeedback} method parameter that all or parts of the repository content be scanned and re-indexed. (The
* repository does the scanning efficiently: if multiple providers request that the entire repository be scanned, the repository
* will index the content only once.) Also, each {@link ManagedIndex} resulting from these methods will be given its own even
* listener, and this is how the indexes are notified of changes in content (see below).
* <li>
* <li>If some content must be scanned, the repository will call {@link #getIndexWriter()} and use the {@link IndexWriter} to
* notify the index(es) of the content.</li>
* </ol>
* After the repository is running, calls to {@link IndexManager} will alter the {@link IndexDefinition}s index definitions. As
* these changes are persisted, the repository will call the
* {@link #notify(IndexDefinitionChanges, ChangeBus, org.modeshape.jcr.NodeTypes.Supplier, Set, IndexFeedback)} again, this time
* with only the changes that were made to this provider's index definitions. If a new workspace is added or an existing workspace
* is removed, ModeShape will called
* {@link #notify(WorkspaceChanges, ChangeBus, org.modeshape.jcr.NodeTypes.Supplier, Set, IndexFeedback)} with the relevant
* information to allow the provider to respond.</li>
* </p>
* <p>
* As sessions persist changes to content (via javax.jcr.Session#save() or by committing user transactions), the repository's
* event bus will notify each {@link ManagedIndex} (via its {@link ManagedIndex#getIndexChangeAdapter() writing adapter}) of these
* changes. This writing adapter is also used during manual re-indexing of content.
* </p>
* <p>
* When queries are submitted, ModeShape will call {@link #getIndexPlanner()} to obtain the planner that should determine for each
* portion of the query which indexes (if any) apply to that part of the query. When a particular index is to be used within a
* query, ModeShape will then call {@link #getIndex(String,String)} and use it to answer the portion of the query.
* </p>
* <h2>Custom providers</h2>
* <p>
* Implementing custom providers is fairly straightforward: simply extend this class, provide implementations for the abstract
* methods, and optionally override the default implementations for the other non-final methods.
* </p>
*
* @author Randall Hauch (rhauch@redhat.com)
*/
public abstract class IndexProvider {
/**
* The default number of rows in a batch.
*/
public static final int DEFAULT_BATCH_SIZE = 100;
private final static IndexWriter EMPTY_WRITER = NoOpQueryIndexWriter.INSTANCE;
/**
* The logger instance, set via reflection
*/
private Logger logger;
/**
* The name of this provider, set via reflection
*/
private String name;
/**
* The execution context, set via reflection
*/
private ExecutionContext context;
/**
* The running environment, set via reflection
*/
private Environment environment;
/**
* The name of the repository that owns this provider, set via reflection
*/
private String repositoryName;
/**
* The name of the system workspace, set via reflection
*/
private String systemWorkspaceName;
/**
* A flag that tracks whether {@link #initialize()} has been called.
*/
private boolean initialized = false;
/**
* The index planner.
*/
private final IndexPlanner planner;
/**
* The {@link AtomicIndex} instances (one in each applicable workspace) all keyed by the index name. Each
* {@link AtomicIndex} is a {@link ChangeSetListener} that is registered with the {@link Observable}, and which forwards
* applicable {@link ChangeSet}s to the provider-supplied {@link ChangeSetListener listener}. It also maintains the
* provider-supplied operations and a reference to the current {@link IndexDefinition}. These {@link AtomicIndex}es are
* managed entirely by this class, and updated based upon the {@link #createIndex}, {@link #updateIndex}, and
* {@link #removeIndex} methods of the provider.
*/
private final Map<String, Map<String, AtomicIndex>> providedIndexesByWorkspaceNameByIndexName = new HashMap<>();
private final Map<String, Map<String, AtomicIndex>> providedIndexesByIndexNameByWorkspaceName = new HashMap<>();
/**
* An IndexWriter that does the work for this provider. This is {@link #refreshDelegateIndexWriter(org.modeshape.jcr.NodeTypes.Supplier) updated} every time the
* {@link #providedIndexesByWorkspaceNameByIndexName provided indexes} are modified, and it is called by the
* publicly-accessible {@link #publicWriter}. Never null.
*/
private volatile IndexWriter delegateWriter = EMPTY_WRITER;
@SuppressWarnings( "synthetic-access" )
private final IndexWriter publicWriter = new IndexWriter() {
@Override
public boolean canBeSkipped() {
return delegateWriter.canBeSkipped();
}
@Override
public void clearAllIndexes() {
delegateWriter.clearAllIndexes();
}
@Override
public boolean add( String workspace,
NodeKey key,
Path path,
Name primaryType,
Set<Name> mixinTypes,
Properties properties ) {
return delegateWriter.add(workspace, key, path, primaryType, mixinTypes, properties);
}
@Override
public boolean remove( String workspace, NodeKey key ) {
return delegateWriter.remove(workspace, key);
}
@Override
public void commit( String workspace ) {
delegateWriter.commit(workspace);
}
};
protected IndexProvider() {
this.planner = new BasicPlanner();
}
protected IndexProvider( IndexPlanner planner ) {
assert planner != null;
this.planner = planner;
}
protected final Logger logger() {
return logger;
}
/**
* Get the name for this provider.
*
* @return the name; never null
*/
public final String getName() {
return name;
}
/**
* Get the name of the repository.
*
* @return the repository name; never null
*/
public final String getRepositoryName() {
return repositoryName;
}
/**
* Get the default number of rows in a batch.
*
* @return the number of rows, should never be < 0;
*/
public int batchSize() {
return DEFAULT_BATCH_SIZE;
}
/**
* Get all the index definition names belonging to this provider.
*
* @return a {@link Set} of index names
*/
public Set<String> getIndexNames() {
return new HashSet<>(providedIndexesByIndexNameByWorkspaceName.keySet());
}
/**
* Get the context in which this provider executes. This is set prior to {@link #initialize() initialization} by ModeShape,
* and never changed.
*
* @return the execution context; never null
*/
protected final ExecutionContext context() {
return context;
}
/**
* Get the repository's running environment
*
* @return a {@link Environment} instance, never null.
*/
protected final Environment environment() {
return environment;
}
/**
* Initialize the provider. This is called automatically by ModeShape once for each provider instance, and should not be
* called by the provider itself.
*
* @throws RepositoryException if there is a problem initializing the provider
*/
public synchronized final void initialize() throws RepositoryException {
if (!initialized) {
try {
doInitialize();
initialized = true;
} catch (RuntimeException e) {
throw new RepositoryException(e);
}
}
}
/**
* Method called by the code calling {@link #initialize} (typically via reflection) to signal that the initialize method is
* completed. See initialize() for details, and no this method is indeed used.
*/
@SuppressWarnings( "unused" )
private void postInitialize() {
if (!initialized) {
initialized = true;
// ------------------------------------------------------------------------------------------------------------
// Add any code here that needs to run after #initialize(...), which will be overwritten by subclasses
// ------------------------------------------------------------------------------------------------------------
}
}
/**
* Method that should do the provider-specific initialization work. This is called by ModeShape once each time the repository
* is started; this method should not be called by the provider itself.
* <p>
* By the time this method is called, ModeShape will hav already set the {@link #context}, {@link #logger}, {@link #name}, and
* {@link #repositoryName} plus any fields that match configuration properties for the provider.
* </p>
* <p>
* This is an excellent place for providers to validate the provider-specific fields set by ModeShape via reflection during
* instantiation.
* </p>
*
* @throws RepositoryException if there is a problem initializing the provider
*/
protected abstract void doInitialize() throws RepositoryException;
/**
* Signal this provider that it is no longer needed and can release any resources that are being held.
*
* @throws RepositoryException if there is a problem shutting down the provider
*/
public synchronized final void shutdown() throws RepositoryException {
preShutdown();
delegateWriter = NoOpQueryIndexWriter.INSTANCE;
try {
// Shutdown each of the provided indexes ...
for (Map<String, AtomicIndex> byWorkspaceName : providedIndexesByWorkspaceNameByIndexName.values()) {
for (AtomicIndex provided : byWorkspaceName.values()) {
provided.shutdown(false);
}
}
} finally {
providedIndexesByWorkspaceNameByIndexName.clear();
providedIndexesByIndexNameByWorkspaceName.clear();
postShutdown();
}
}
/**
* Method called immediately when #shutdown() is invoked, before any other operations are performed and before the managed
* indexes are each shutdown.
*
* @throws RepositoryException if there is a problem shutting down the provider
*/
protected void preShutdown() throws RepositoryException {
// Do nothing by default
}
/**
* Method called during #shutdown() after each of the managed indexes have been shutdown.
*
* @throws RepositoryException if there is a problem shutting down the provider
*/
protected void postShutdown() throws RepositoryException {
// Do nothing by default
}
/**
* Notify the provider that the NodeTypes have changed. This method should only be called by ModeShape and never called by
* other code.
*
* @param updatedNodeTypes the new node types; may not be null
*/
public void notify( final NodeTypes updatedNodeTypes ) {
CheckArg.isNotNull(updatedNodeTypes, "updatedNodeTypes");
// For each of the provided indexes ...
onEachIndex(new ProvidedIndexOperation() {
@Override
public void apply( String workspaceName,
AtomicIndex index ) {
@SuppressWarnings( "synthetic-access" )
NodeTypeMatcher matcher = nodeTypePredicate(updatedNodeTypes, index.indexDefinition());
index.update(index.managed(), index.indexDefinition(), matcher);
}
});
}
/**
* Validate the proposed index definition, and use the supplied problems to report any issues that will prevent this provider
* from creating and using an index with the given definition.
* <p>
* This should only be implemented if the provider has any particular validation requirements (for example it doesn't support
* multiple columns or certain index types). The repository will perform for all index definitions some basic validations
* via {@link #validateDefaultColumnTypes(ExecutionContext, IndexDefinition, Problems)}
* </p>
*
* @param context the execution context in which to perform the validation; never null
* @param defn the proposed index definition; never null
* @param nodeTypesSupplier the supplier for the NodeTypes object that contains information about the currently-registered
* node types; never null
* @param problems the problems that should be used to report any issues with the index definition; never null
*/
public void validateProposedIndex( ExecutionContext context,
IndexDefinition defn,
NodeTypes.Supplier nodeTypesSupplier,
Problems problems ) {
//nothing by default
}
/**
* Validates that if certain default columns are present in the index definition, they have a required type.
*
* @param context the execution context in which to perform the validation; never null
* @param defn the proposed index definition; never null
* @param problems the component to record any problems, errors, or warnings; may not be null
*/
public void validateDefaultColumnTypes( ExecutionContext context,
IndexDefinition defn,
Problems problems ) {
assert defn != null;
for (int i = 0; i < defn.size(); i++) {
validateDefaultColumnDefinitionType(context, defn, defn.getColumnDefinition(i), problems);
}
}
protected void validateDefaultColumnDefinitionType( ExecutionContext context,
IndexDefinition defn,
IndexColumnDefinition columnDefn,
Problems problems) {
int columnType = columnDefn.getColumnType();
PropertyType type = PropertyType.valueFor(columnType);
switch (defn.getKind()) {
case UNIQUE_VALUE:
case ENUMERATED_VALUE:
case VALUE:
if ((matches(context, columnDefn, JcrLexicon.PATH) && !isType(type, PropertyType.PATH))) {
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, PropertyType.PATH);
}
if ((matches(context, columnDefn, ModeShapeLexicon.LOCALNAME) && !isType(type, PropertyType.STRING))
|| (matches(context, columnDefn, ModeShapeLexicon.ID) && !isType(type, PropertyType.STRING))) {
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, PropertyType.STRING);
}
if ((matches(context, columnDefn, ModeShapeLexicon.DEPTH) && !isType(type, PropertyType.LONG))) {
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, PropertyType.LONG);
}
if ((matches(context ,columnDefn, JcrLexicon.PRIMARY_TYPE) && !isType(type, PropertyType.NAME))
|| (matches(context, columnDefn, JcrLexicon.MIXIN_TYPES) && !isType(type, PropertyType.NAME))
|| (matches(context, columnDefn, JcrLexicon.NAME) && !isType(type, PropertyType.NAME))) {
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, PropertyType.NAME);
}
break;
case NODE_TYPE:
if (PropertyType.STRING != type) {
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, PropertyType.STRING);
}
break;
case TEXT: {
if (PropertyType.STRING != type && PropertyType.BINARY != type && PropertyType.PATH != type &&
PropertyType.NAME != type) {
String expectedTypeMsg = PropertyType.STRING + " or " + PropertyType.BINARY;
problems.addError(JcrI18n.indexMustHaveOneColumnOfSpecificType, defn.getProviderName(),
defn.getName(), columnDefn.getPropertyName(), type, expectedTypeMsg);
}
break;
}
}
}
protected final boolean matches( ExecutionContext context,
IndexColumnDefinition defn,
Name name ) {
return defn.getPropertyName().equals(name.getString(context.getNamespaceRegistry()));
}
protected final boolean matches( ExecutionContext context,
String actual,
Name name ) {
return actual.equals(name.getString(context.getNamespaceRegistry()));
}
protected final boolean isType( PropertyType propType,
PropertyType expected ) {
return propType == expected;
}
/**
* Get the writer that ModeShape can use to regenerate the indexes when a portion of the repository is to be re-indexed.
*
* @return the index writer; may be null if the indexes are updated outside of ModeShape
*/
public final IndexWriter getIndexWriter() {
return publicWriter;
}
/**
* Get the queryable index with the given name and applicable for the given workspace.
*
* @param indexName the name of the index in this provider; never null
* @param workspaceName the name of the workspace; never null
* @return the queryable index, or null if there is no such index
*/
public final Index getIndex( String indexName,
String workspaceName ) {
logger().trace("Looking for index '{0}' in '{1}' provider for query in workspace '{2}'", indexName, getName(),
workspaceName);
Map<String, AtomicIndex> byWorkspaceNames = providedIndexesByWorkspaceNameByIndexName.get(indexName);
return byWorkspaceNames == null ? null : byWorkspaceNames.get(workspaceName);
}
/**
* Get the managed index with the given name and applicable for the given workspace.
*
* @param indexName the name of the index in this provider; never null
* @param workspaceName the name of the workspace; never null
* @return the managed index, or null if there is no such index
*/
public final ManagedIndex getManagedIndex( String indexName,
String workspaceName ) {
logger().trace("Looking for managed index '{0}' in '{1}' provider in workspace '{2}'", indexName, getName(),
workspaceName);
Map<String, AtomicIndex> byWorkspaceNames = providedIndexesByWorkspaceNameByIndexName.get(indexName);
if (byWorkspaceNames == null) {
return null;
}
AtomicIndex atomicIndex = byWorkspaceNames.get(workspaceName);
return atomicIndex == null ? null : atomicIndex.managed();
}
/**
* Return the latest time at which this provider successfully updated its indexes.
*
* @return a {@code Long} instance which either contains the latest update time or {@code null} which means that
* this provider does not support incremental reindexing.
*
*/
public Long getLatestIndexUpdateTime() {
// by default don't support incremental reindexing
return null;
}
/**
* Get this provider's {@link ManagedIndex} instances for the given workspace.
*
* @param workspaceName the name of the workspace; may not be null
* @return the iterator over the provided indexes; never null but possibly empty
*/
protected final Iterable<ManagedIndex> getIndexes( String workspaceName ) {
final Map<String, AtomicIndex> byIndexName = providedIndexesByIndexNameByWorkspaceName.get(workspaceName);
if (byIndexName == null) return Collections.emptySet();
return DelegateIterable.around(byIndexName.values(), new Function<AtomicIndex, ManagedIndex>() {
@Override
public ManagedIndex apply( AtomicIndex input ) {
return input.managed();
}
});
}
/**
* Perform the specified operation on each of the managed indexes.
*
* @param op the operation; may not be null
*/
protected final void onEachIndex( ManagedIndexOperation op ) {
for (String workspaceName : workspaceNames()) {
onEachIndexInWorkspace(workspaceName, op);
}
}
/**
* Perform the specified operation on each of the managed indexes.
*
* @param op the operation; may not be null
*/
private final void onEachIndex( ProvidedIndexOperation op ) {
for (String workspaceName : workspaceNames()) {
Collection<AtomicIndex> indexes = providedIndexesFor(workspaceName);
if (indexes != null) {
for (AtomicIndex atomicIndex : indexes) {
assert atomicIndex.managed() != null;
assert atomicIndex.indexDefinition() != null;
op.apply(workspaceName, atomicIndex);
}
}
}
}
private synchronized Set<String> workspaceNames() {
return new HashSet<String>(providedIndexesByIndexNameByWorkspaceName.keySet());
}
private synchronized Collection<AtomicIndex> providedIndexesFor( String workspaceName ) {
Map<String, AtomicIndex> byIndexName = providedIndexesByIndexNameByWorkspaceName.get(workspaceName);
if (byIndexName != null) {
return new ArrayList<>(byIndexName.values());
}
return null;
}
/**
* Perform the specified operation on each of the managed indexes in the named workspace.
*
* @param workspaceName the name of the workspace; may not be null
* @param op the operation; may not be null
*/
public final void onEachIndexInWorkspace( String workspaceName,
ManagedIndexOperation op ) {
assert workspaceName != null;
Collection<AtomicIndex> indexes = providedIndexesFor(workspaceName);
if (indexes != null) {
for (AtomicIndex atomicIndex : indexes) {
assert atomicIndex.managed() != null;
assert atomicIndex.indexDefinition() != null;
op.apply(workspaceName, atomicIndex.managed(), atomicIndex.indexDefinition());
}
}
}
/**
* An operation that performs on a managed index with the associated index definition.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
public static interface ManagedIndexOperation {
/**
* Apply the operation to a managed index
*
* @param workspaceName the name of the workspace in which the index exists; may not be null
* @param index the managed index instance; may not be null
* @param defn the definition for the index
*/
void apply( String workspaceName,
ManagedIndex index,
IndexDefinition defn );
}
/**
* An operation that performs on a provided index with the associated index definition.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
private static interface ProvidedIndexOperation {
/**
* Apply the operation to a provided index
*
* @param workspaceName the name of the workspace in which the index exists; may not be null
* @param index the managed index instance; may not be null
*/
void apply( String workspaceName,
AtomicIndex index );
}
/**
* An IndexPlanner that calls {@link IndexProvider#planUseOfIndex} on each applicable managed index.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
private final class BasicPlanner extends IndexPlanner {
protected BasicPlanner() {
}
@Override
public void applyIndexes( final QueryContext context,
final IndexCostCalculator calculator ) {
final ManagedIndexOperation planningOp = new ManagedIndexOperation() {
@Override
public void apply( String workspaceName,
ManagedIndex index,
IndexDefinition defn ) {
boolean traceEnabled = logger().isTraceEnabled();
if (!defn.isEnabled()) {
if (traceEnabled) {
logger().trace("Skipping index '{0}' in '{1}' provider because it is not enabled",
defn.getName(), getName());
}
return;
}
if (!defn.getWorkspaceMatchRule().usedInWorkspace(workspaceName)) {
if (traceEnabled) {
logger().trace("Skipping index '{0}' in '{1}' provider for query because it doest not match the '{2}' workspace",
defn.getName(), getName(), workspaceName);
}
return;
}
if (matchesType(defn, calculator, context)) {
if (traceEnabled) {
logger().trace("Considering index '{0}' in '{1}' provider for query in workspace '{2}'",
defn.getName(),
getName(),
workspaceName);
}
planUseOfIndex(context, calculator, workspaceName, index, defn);
} else if (traceEnabled) {
logger().trace("Skipping index '{0}' in '{1}' provider for query because the index definition node type '{2}' does not match the selected node types '{3}'",
defn.getName(), getName(), defn.getNodeTypeName(), calculator.selectedNodeTypes());
}
}
};
for (String workspaceName : context.getWorkspaceNames()) {
onEachIndexInWorkspace(workspaceName, planningOp);
}
}
}
private boolean matchesType( IndexDefinition defn,
IndexCostCalculator calculator,
QueryContext context ) {
final NodeTypes nodeTypes = context.getNodeTypes();
final NameFactory nameFactory = context.getExecutionContext().getValueFactories().getNameFactory();
// check of the node type of the index defn matches the request node type(s) from the query
String indexedNodeType = defn.getNodeTypeName();
Name indexedNodeTypeName = nameFactory.create(indexedNodeType);
Set<String> selectedNodeTypes = calculator.selectedNodeTypes();
assert !selectedNodeTypes.isEmpty();
for (String nodeTypeOrAlias : selectedNodeTypes) {
Name selectedNodeTypeName = nameFactory.create(nodeTypeOrAlias);
if (nodeTypes.isTypeOrSubtype(selectedNodeTypeName, indexedNodeTypeName)) {
return true;
}
}
return false;
}
/**
* The method that is called by the IndexProvider's {@link IndexProvider#getIndexPlanner default IndexPlanner} for each
* managed index in the given workspace.
* <p>
* Subclasses can implement this method to determine and record whether the supplied index can be used by the query against
* the named workspace, but usually the default implementation should be good enough. Subclasses should really only implement
* the {@link #evaluateUsage(QueryContext, IndexCostCalculator, IndexDefinition)} method.
* </p>
* The {@link IndexUsage} class is useful to determine for a given index definition whether criteria is applicable.
*
* @param context the context of the original query; never null
* @param calculator the calculator that should be used to record plan information for the index; never null
* @param workspaceName the name of the workspace against which the query is operating; never null
* @param index the managed index in the given workspace; never null
* @param defn the definition for the index; never null
*/
protected void planUseOfIndex( QueryContext context,
IndexCostCalculator calculator,
String workspaceName,
ManagedIndex index,
IndexDefinition defn ) {
// Ask the provider for an index usage component
IndexUsage planner = evaluateUsage(context, calculator, defn);
if (planner == null) {
throw new UnsupportedOperationException("Providers should either override this method or the #evaluateUsage method");
}
// Does this index apply to any of the ANDed constraints?
int costEstimate = getCostEstimate();
List<Constraint> applicableConstraints = new ArrayList<>();
for (Constraint constraint : calculator.andedConstraints()) {
if (planner.indexAppliesTo(constraint)) {
logger().trace("Index '{0}' in '{1}' provider applies to query in workspace '{2}' with constraint: {3}",
defn.getName(), getName(), workspaceName, constraint);
// The index does apply to this constraint ...
applicableConstraints.add(constraint);
}
}
if (!applicableConstraints.isEmpty()) {
long cardinality = index.estimateCardinality(applicableConstraints, context.getVariables());
long total = index.estimateTotalCount();
Float selectivity = null;
if (total > 0L) {
double ratio = (double)cardinality / (double)total;
selectivity = cardinality <= total ? new Float(ratio) : IndexCostCalculator.MAX_SELECTIVITY;
}
calculator.addIndex(defn.getName(), workspaceName, getName(), applicableConstraints, costEstimate, cardinality,
selectivity);
}
Collection<JoinCondition> joinConditions = calculator.joinConditions();
if (joinConditions.isEmpty()) {
return;
}
List<JoinCondition> applicableJoins = new ArrayList<>();
// Does this index apply to any of the join conditions ...
for (JoinCondition joinCondition : joinConditions) {
if (planner.indexAppliesTo(joinCondition)) {
logger().trace("Index '{0}' in '{1}' provider applies to query in workspace '{2}' with constraint: {3}",
defn.getName(), getName(), workspaceName, joinCondition);
applicableJoins.add(joinCondition);
}
}
if (!applicableJoins.isEmpty()) {
// The index does apply to this constraint, but the number of values corresponds to the total number of values
// in the index (this is a JOIN CONDITON for which there is no literal values) ...
long total = index.estimateTotalCount();
calculator.addIndex(defn.getName(), workspaceName, getName(), applicableJoins, costEstimate, total);
}
}
/**
* Returns an object which is used during the planning phase to evaluate if a certain index should be used or not.
*
* @param context the context of the original query; never null
* @param calculator the calculator that should be used to record plan information for the index; never null
* @param defn the definition for the index; never null
* @return an {@link IndexUsage} instance, or {@code null} which is the default and which indicates that a provider chooses
* to override the {@link #planUseOfIndex(QueryContext, IndexCostCalculator, String, ManagedIndex, IndexDefinition)} method
*
* @see #planUseOfIndex(QueryContext, IndexCostCalculator, String, ManagedIndex, IndexDefinition)
*/
protected IndexUsage evaluateUsage( QueryContext context,
IndexCostCalculator calculator,
IndexDefinition defn ) {
// this returns null for backwards compatibility reasons (otherwise it should've been abstract)
return null;
}
/**
* Returns a positive number representing the cost estimate of using indexes from this provider. The cost estimate is a
* relative measure of how fast/complex the indexes of a provider are which should also take into account whether a provider
* is local to the repository or not.
*
* Remote providers should always return a higher cost than local providers.
*
* @return a positive integer
* @see org.modeshape.jcr.spi.index.IndexCostCalculator.Costs
*/
protected int getCostEstimate() {
return IndexCostCalculator.Costs.LOCAL;
}
/**
* Get the planner that, during the query planning/optimization phase, evaluates for a single source the AND-ed query
* constraints and defines indexes that may be used.
* <p>
* This method is typically called only once after the provider has been {@link #initialize() initialized}.
* </p>
*
* @return the index planner; may not be null
*/
public final IndexPlanner getIndexPlanner() {
return planner;
}
/**
* Get the namespace registry for the provider's {@link #context() execution context}, and which can be used to convert
* qualified names (e.g., "{@code jcr:description}") to unqualified names (or vice versa).
*
* @return the namespace registry; never null
*/
protected final NamespaceRegistry namespaces() {
return context.getNamespaceRegistry();
}
/**
* Get the container for the type-specific factories useful to convert values that will be written to the index.
*
* @return the container of type-specific value factories; never null
*/
protected final ValueFactories valueFactories() {
return context.getValueFactories();
}
protected final NameFactory names() {
return valueFactories().getNameFactory();
}
/**
* Signal that some workspaces were added or removed. This method is also called upon startup of this repository instance so
* that the provider understands the index definitions that are available. The provider should adapt to these changes as best
* as possible.
* <p>
* This method examines the supplied {@link WorkspaceChanges workspace additions and removals}, and updates the internal state
* of this provider. It will then call the {@link #createIndex}, {@link #updateIndex}, or {@link #removeIndex} methods for
* each of the affected indexes.
* </p>
*
* @param changes the changes in the workspaces; never null
* @param observable the Observable object with which an index can register a listener for changes; this is the only mechanism
* by which the indexes can be updated
* @param nodeTypesSupplier the supplier from which can be very efficiently obtained the latest {@link NodeTypes snapshot of
* node types} useful in determining if changes on a node are to be included in an index of a particular
* {@link IndexDefinition#getNodeTypeName() index node type}.
* @param workspaceNames the names of all workspaces in this repository; never null and likely never null
* @param feedback the feedback mechanism for this provider to signal to ModeShape that one or more indexes need to be
* entirely or partially rebuilt via scanning; never null
*/
public synchronized final void notify( final WorkspaceChanges changes,
ChangeBus observable,
NodeTypes.Supplier nodeTypesSupplier,
final Set<String> workspaceNames,
IndexFeedback feedback ) {
for (IndexDefinition defn : changes.getIndexDefinitions()) {
for (String workspaceName : changes.getAddedWorkspaces()) {
if (defn.getWorkspaceMatchRule().usedInWorkspace(workspaceName)) {
// Add the index ...
try {
NodeTypeMatcher matcher = nodeTypePredicate(nodeTypesSupplier.getNodeTypes(), defn);
ManagedIndex managedIndex = createIndex(defn, workspaceName, nodeTypesSupplier, matcher, feedback);
AtomicIndex index = new AtomicIndex(defn, managedIndex, workspaceName, matcher);
addProvidedIndex(index);
registerIndex(index, observable);
} catch (RuntimeException e) {
String msg = "Error updating index '{0}' in workspace '{1}' with definition: {2}";
logger().error(e, msg, defn.getName(), workspaceName, defn);
}
}
}
}
final Set<String> removedWorkspaces = changes.getRemovedWorkspaces();
if (!removedWorkspaces.isEmpty()) {
// Remove the managed indexes for workspaces that no longer exist ...
removeProvidedIndexes(observable, new Predicate<AtomicIndex>() {
@Override
public boolean test(AtomicIndex index) {
return removedWorkspaces.contains(index.workspaceName());
}
});
}
refreshDelegateIndexWriter(nodeTypesSupplier);
}
/**
* Signal that some of the definitions of indexes owned by this provider were changed. This method is also called upon startup
* of this repository instance so that the provider understands the index definitions that are available. The provider should
* adapt to these changes as best as possible.
* <p>
* This method examines the supplied {@link IndexDefinitionChanges changes}, current workspaces, and maintains the internal
* state of this provider. It will then call the {@link #createIndex}, {@link #updateIndex}, or {@link #removeIndex} methods
* for each of the affected indexes.
* </p>
*
* @param changes the changes in the definitions; never null
* @param observable the Observable object with which an index can register a listener for changes; this is the only mechanism
* by which the indexes can be updated
* @param nodeTypesSupplier the supplier from which can be very efficiently obtained the latest {@link NodeTypes snapshot of
* node types} useful in determining if changes on a node are to be included in an index of a particular
* {@link IndexDefinition#getNodeTypeName() index node type}.
* @param workspaceNames the names of all workspaces in this repository; never null and likely never null
* @param feedback the feedback mechanism for this provider to signal to ModeShape that one or more indexes need to be
* entirely or partially rebuilt via scanning; never null
*/
public synchronized final void notify( final IndexDefinitionChanges changes,
ChangeBus observable,
NodeTypes.Supplier nodeTypesSupplier,
final Set<String> workspaceNames,
IndexFeedback feedback ) {
for (IndexDefinition defn : changes.getUpdatedIndexDefinitions().values()) {
Map<String, AtomicIndex> providedIndexesByWorkspaceName = providedIndexesByWorkspaceNameByIndexName.get(defn.getName());
if (providedIndexesByWorkspaceName == null || providedIndexesByWorkspaceName.isEmpty()) {
// There are no managed indexes for this index definition, so we know the index(es) will be new ...
for (String workspaceName : workspaceNames) {
if (defn.getWorkspaceMatchRule().usedInWorkspace(workspaceName)) {
// Add the index ...
try {
NodeTypeMatcher matcher = nodeTypePredicate(nodeTypesSupplier.getNodeTypes(), defn);
ManagedIndex managedIndex = createIndex(defn, workspaceName, nodeTypesSupplier, matcher, feedback);
AtomicIndex index = new AtomicIndex(defn, managedIndex, workspaceName, matcher);
addProvidedIndex(index);
registerIndex(index, observable);
} catch (RuntimeException e) {
String msg = "Error updating index '{0}' in workspace '{1}' with definition: {2}";
logger().error(e, msg, defn.getName(), workspaceName, defn);
}
}
}
} else {
// There is at least one existing managed index for this index definition, so figure out whether they should
// be updated, created, or removed ...
assert providedIndexesByWorkspaceName != null && !providedIndexesByWorkspaceName.isEmpty();
for (String workspaceName : workspaceNames) {
AtomicIndex provided = providedIndexesByWorkspaceName.get(workspaceName);
if (provided != null) {
if (defn.getWorkspaceMatchRule().usedInWorkspace(workspaceName)) {
IndexDefinition oldDefn = provided.indexDefinition();
if (!isChanged(oldDefn, defn)) {
// Nothing about the index definition that we care about really changed, so don't do anything ...
logger().debug("Index provider '{0}' is not updating index in workspace '{1}' because there were no changes: {2}",
getName(), workspaceName, defn);
continue;
}
// The index is updated and still applies to this workspace, so update the operations ...
try {
NodeTypeMatcher matcher = nodeTypePredicate(nodeTypesSupplier.getNodeTypes(), defn);
ManagedIndex managedIndex = updateIndex(oldDefn, defn, provided.managed(),
workspaceName, nodeTypesSupplier, matcher, feedback);
provided.update(managedIndex, defn, matcher);
} catch (RuntimeException e) {
String msg = "Error updating index '{0}' in workspace '{1}' with definition: {2}";
logger().error(e, msg, defn.getName(), workspaceName, defn);
}
} else {
// The existing managed index no longer applies to this workspace ...
removeProvidedIndex(provided, observable);
}
} else {
// There is no managed index yet, so add one ...
try {
NodeTypeMatcher matcher = nodeTypePredicate(nodeTypesSupplier.getNodeTypes(), defn);
ManagedIndex managedIndex = createIndex(defn, workspaceName, nodeTypesSupplier, matcher, feedback);
AtomicIndex index = new AtomicIndex(defn, managedIndex, workspaceName, matcher);
addProvidedIndex(index);
registerIndex(index, observable);
} catch (RuntimeException e) {
String msg = "Error adding index '{0}' in workspace '{1}' with definition: {2}";
logger().error(e, msg, defn.getName(), workspaceName, defn);
}
}
}
// Remove the managed indexes for workspaces that no longer exist ...
removeProvidedIndexes(observable, new Predicate<AtomicIndex>() {
@Override
public boolean test( AtomicIndex index ) {
return !workspaceNames.contains(index.workspaceName());
}
});
}
}
final Set<String> removedIndexDefinitions = changes.getRemovedIndexDefinitions();
if (!removedIndexDefinitions.isEmpty()) {
// Remove all of the managed indexes for REMOVED index definitions ...
removeProvidedIndexes(observable, new Predicate<AtomicIndex>() {
@Override
public boolean test(AtomicIndex index) {
return removedIndexDefinitions.contains(index.getName());
}
});
}
refreshDelegateIndexWriter(nodeTypesSupplier);
}
private boolean isChanged( IndexDefinition defn1,
IndexDefinition defn2 ) {
if (defn1.getKind() != defn2.getKind()) return true;
if (defn1.size() != defn2.size()) return true;
for (int i = 0; i != defn1.size(); ++i) {
IndexColumnDefinition col1 = defn1.getColumnDefinition(i);
IndexColumnDefinition col2 = defn2.getColumnDefinition(i);
if (isChanged(col1, col2)) return true;
}
// We don't care about any properties ...
return false;
}
private boolean isChanged( IndexColumnDefinition defn1,
IndexColumnDefinition defn2 ) {
if (defn1.getColumnType() != defn2.getColumnType()) return true;
if (!defn1.getPropertyName().equals(defn2.getPropertyName())) return true;
return false;
}
@GuardedBy( "this" )
private void addProvidedIndex( AtomicIndex index ) {
String indexName = index.getName();
String workspaceName = index.workspaceName();
Map<String, AtomicIndex> managedIndexesByWorkspaceName = providedIndexesByWorkspaceNameByIndexName.get(indexName);
if (managedIndexesByWorkspaceName == null) {
managedIndexesByWorkspaceName = new HashMap<>();
providedIndexesByWorkspaceNameByIndexName.put(indexName, managedIndexesByWorkspaceName);
}
managedIndexesByWorkspaceName.put(workspaceName, index);
// Add it to the reverse lookup ...
Map<String, AtomicIndex> byName = providedIndexesByIndexNameByWorkspaceName.get(workspaceName);
if (byName == null) {
byName = new HashMap<>();
providedIndexesByIndexNameByWorkspaceName.put(workspaceName, byName);
}
byName.put(indexName, index);
}
private void scanWorkspace(IndexFeedback feedback,
final IndexDefinition defn,
final String workspaceName,
final ManagedIndex managedIndex,
final NodeTypes.Supplier nodeTypesSupplier) {
feedback.scan(workspaceName, new IndexFeedback.IndexingCallback() {
@SuppressWarnings( "synthetic-access" )
@Override
public void beforeIndexing() {
logger().debug(
"Disabling index '{0}' from provider '{1}' in workspace '{2}' while it is reindexed. It will not be used in queries until reindexing has completed",
defn.getName(), defn.getProviderName(), workspaceName);
managedIndex.enable(false);
}
@SuppressWarnings( "synthetic-access" )
@Override
public void afterIndexing() {
managedIndex.enable(true);
logger().debug("Enabled index '{0}' from provider '{1}' in workspace '{2}' after reindexing has completed",
defn.getName(), defn.getProviderName(), workspaceName);
}
@Override
public IndexWriter writer() {
return new IndexWriter() {
@Override
public boolean canBeSkipped() {
return false;
}
@Override
public void clearAllIndexes() {
managedIndex.clearAllData();
}
@Override
public boolean add(String workspace, NodeKey key, Path path, Name primaryType, Set<Name> mixinTypes,
Properties properties) {
boolean queryable = nodeTypesSupplier.getNodeTypes().isQueryable(primaryType, mixinTypes);
return managedIndex.getIndexChangeAdapter().reindex(workspace, key, path, primaryType, mixinTypes, properties,
queryable);
}
@Override
public boolean remove(String workspace, NodeKey key) {
managedIndex.getIndexChangeAdapter().clearDataFor(key);
return true;
}
@Override
public void commit(String workspace) {
managedIndex.getIndexChangeAdapter().index().commit();
}
};
}
});
}
@GuardedBy( "this" )
private void refreshDelegateIndexWriter(final NodeTypes.Supplier nodeTypesSupplier ) {
// Go through the providers and assemble into a structure that a new IndexWriter can use ...
final Map<String, Collection<IndexChangeAdapter>> adaptersByWorkspaceName = new HashMap<>();
final Collection<ManagedIndex> managedIndexes = new ArrayList<>();
for (Map<String, AtomicIndex> providedIndexesByWorkspaceName : providedIndexesByWorkspaceNameByIndexName.values()) {
for (Map.Entry<String, AtomicIndex> entry : providedIndexesByWorkspaceName.entrySet()) {
String workspaceName = entry.getKey();
AtomicIndex index = entry.getValue();
Collection<IndexChangeAdapter> adaptersForWorkspace = adaptersByWorkspaceName.get(workspaceName);
if (adaptersForWorkspace == null) {
adaptersForWorkspace = new ArrayList<>();
adaptersByWorkspaceName.put(workspaceName, adaptersForWorkspace);
}
adaptersForWorkspace.add(index.managed().getIndexChangeAdapter());
managedIndexes.add(index.managed());
}
}
final boolean canBeSkipped = managedIndexes.isEmpty();
// Create a delegate writer ...
this.delegateWriter = new IndexWriter() {
@Override
public boolean canBeSkipped() {
return canBeSkipped;
}
@Override
public void clearAllIndexes() {
for (ManagedIndex index : managedIndexes) {
index.clearAllData();
}
}
@Override
public boolean add( String workspace,
NodeKey key,
Path path,
Name primaryType,
Set<Name> mixinTypes,
Properties properties ) {
Collection<IndexChangeAdapter> adapters = applicableAdapters(workspace);
boolean indexesUpdated = false;
if (adapters != null) {
boolean queryable = nodeTypesSupplier.getNodeTypes().isQueryable(primaryType, mixinTypes);
// There are adapters for this workspace ...
for (IndexChangeAdapter adapter : adapters) {
if (adapter != null) {
indexesUpdated |= adapter.reindex(workspace, key, path, primaryType, mixinTypes, properties, queryable);
}
}
}
return indexesUpdated;
}
@Override
public boolean remove( String workspace, NodeKey key ) {
Collection<IndexChangeAdapter> adapters = applicableAdapters(workspace);
boolean indexesUpdated = false;
if (adapters != null) {
// There are adapters for this workspace ...
for (IndexChangeAdapter adapter : adapters) {
if (adapter != null) {
adapter.clearDataFor(key);
indexesUpdated = true;
}
}
}
return indexesUpdated;
}
@Override
public void commit( String workspace ) {
Collection<IndexChangeAdapter> adapters = applicableAdapters(workspace);
if (adapters != null) {
for (IndexChangeAdapter adapter : adapters) {
adapter.index().commit();
}
}
}
private Collection<IndexChangeAdapter> applicableAdapters( String workspace ) {
Collection<IndexChangeAdapter> adapters = null;
if (systemWorkspaceName.equals(workspace)) {
// the system workspace is linked to each WS, so all adapters have to process system data...
adapters = new ArrayList<>();
for (Collection<IndexChangeAdapter> adapterCollection : adaptersByWorkspaceName.values()) {
adapters.addAll(adapterCollection);
}
} else {
adapters = adaptersByWorkspaceName.get(workspace);
}
return adapters;
}
};
}
/**
* Method called when this provider needs to create a new index given the unique pair of workspace name and index definition.
* An index definition can apply to multiple workspaces, and when it does this method will be called once for each applicable
* workspace.
* <p>
* Providers may either choose to implement this method and therefore have full control over the logic of creating managed indexes
* or simply implement the {@link #getIndexBuilder(IndexDefinition, String, NodeTypes.Supplier, NodeTypePredicate)}
* method which is the easier route and provides some built-in defaults.
* </p>
*
* @param defn the definition of the index; never null
* @param workspaceName the name of the actual workspace to which the new index applies; never null
* @param nodeTypesSupplier the supplier for the current node types cache; never null
* @param matcher the node type matcher used to determine which nodes should be included in the index, and which automatically
* updates when node types are changed in the repository; may not be null
* @param feedback the feedback mechanism for this provider to signal to ModeShape that portions of the repository content
* must be scanned to build/populate the new index; never null
* @return the implementation-specific {@link ManagedIndex} for the new index; may not be null
*/
protected ManagedIndex createIndex( IndexDefinition defn,
String workspaceName,
NodeTypes.Supplier nodeTypesSupplier,
NodeTypePredicate matcher,
IndexFeedback feedback ) {
ManagedIndexBuilder builder = getIndexBuilder(defn, workspaceName, nodeTypesSupplier, matcher);
if (builder == null) {
throw new UnsupportedOperationException("Index providers should either override this method or the #getIndexBuilder method");
}
logger().debug("Index provider '{0}' is creating index in workspace '{1}': {2}", getName(), workspaceName, defn);
ManagedIndex index = builder.build();
if (index.requiresReindexing()) {
scanWorkspace(feedback, defn, workspaceName, index, nodeTypesSupplier);
}
return index;
}
/**
* Method called when this provider needs to update an existing index given the unique pair of workspace name and index
* definition. An index definition can apply to multiple workspaces, and when it is changed this method will be called once
* for each applicable workspace.
*
* <p>
* Providers may either choose to implement this method and therefore have full control over the logic of updating managed indexes
* or simply implement the {@link #getIndexBuilder(IndexDefinition, String, NodeTypes.Supplier, NodeTypePredicate)}
* method which is the easier route and provides a built-in logic which will first destroy the existing (old) index and then
* create and register a new one via the builder.
* </p>
*
* @param oldDefn the previous definition of the index; never null
* @param updatedDefn the updated definition of the index; never null
* @param existingIndex the existing index prior to this update, as returned from {@link #createIndex} or {@link #updateIndex}
* ; never null
* @param workspaceName the name of the actual workspace to which the new index applies; never null
* @param nodeTypesSupplier the supplier for the current node types cache; never null
* @param matcher the node type matcher used to determine which nodes should be included in the index, and which automatically
* updates when node types are changed in the repository; may not be null
* @param feedback the feedback mechanism for this provider to signal to ModeShape that portions of the repository content
* must be scanned to rebuild/repopulate the updated index; never null
* @return the operations and provider-specific state for this index; never null
*/
protected ManagedIndex updateIndex( IndexDefinition oldDefn,
IndexDefinition updatedDefn,
ManagedIndex existingIndex,
String workspaceName,
NodeTypes.Supplier nodeTypesSupplier,
NodeTypePredicate matcher,
IndexFeedback feedback ) {
ManagedIndexBuilder builder = getIndexBuilder(updatedDefn, workspaceName, nodeTypesSupplier, matcher);
if (builder == null) {
throw new UnsupportedOperationException("Index providers should either override this method or the #getIndexBuilder method");
}
logger().debug("Index provider '{0}' is updating index in workspace '{1}': {2}", getName(), workspaceName, updatedDefn);
existingIndex.shutdown(true);
ManagedIndex index = builder.build();
if (index.requiresReindexing()) {
scanWorkspace(feedback, updatedDefn, workspaceName, index, nodeTypesSupplier);
}
return index;
}
/**
* Method called when this provider needs to remove an existing index given the unique pair of workspace name and index
* definition. An index definition can apply to multiple workspaces, and when it does this method will be called once for each
* applicable workspace.
* <p>
* The default implementation is to simply delegate the call to a specific index's
* {@link org.modeshape.jcr.spi.index.provider.ProvidedIndex#shutdown(boolean)} method, indicating that the index should be destroyed.
* </p>
*
* @param oldDefn the previous definition of the index; never null
* @param existingIndex the existing index prior to this update, as returned from {@link #createIndex} or {@link #updateIndex}
* ; never null
* @param workspaceName the name of the actual workspace to which the new index applies; never null
*/
protected void removeIndex( IndexDefinition oldDefn,
ManagedIndex existingIndex,
String workspaceName ) {
logger().debug("Index provider '{0}' is removing index in workspace '{1}': {2}", getName(), workspaceName, oldDefn);
existingIndex.shutdown(true);
}
/**
* Offers subclasses the possibility of providing a {@link ManagedIndexBuilder} instance which significantly simplifies the
* task of creating or updating {@link ManagedIndex} instances for providers.
* <p>
* If a provider chooses to override this method, it does not have to override
* the {@link #createIndex(IndexDefinition, String, NodeTypes.Supplier, NodeTypePredicate, IndexFeedback)} method or
* the {@link #updateIndex(IndexDefinition, IndexDefinition, ManagedIndex, String, NodeTypes.Supplier, NodeTypePredicate, IndexFeedback)}
* method, as the builder instance will be used to create specific indexes and adapters, based on the index definition.
* </p>
* <p>
* Providers can also choose to ignore this method, in which case they will have to override the
* {@link #createIndex(IndexDefinition, String, NodeTypes.Supplier, NodeTypePredicate, IndexFeedback)} and
* {@link #updateIndex(IndexDefinition, IndexDefinition, ManagedIndex, String, NodeTypes.Supplier, NodeTypePredicate, IndexFeedback)}
* methods.
* </p>
*
* @param defn the definition of the index; never null
* @param workspaceName the name of the actual workspace to which the new index applies; never null
* @param nodeTypesSupplier the supplier for the current node types cache; never null
* @param matcher the node type matcher used to determine which nodes should be included in the index, and which automatically
* updates when node types are changed in the repository; may not be null
* @return a {@link ManagedIndexBuilder} instance, or {@code null} (the default) which indicates that a provider chooses to
* ovveride both the createIndex and updateIndex methods.
*/
protected ManagedIndexBuilder getIndexBuilder( IndexDefinition defn,
String workspaceName,
NodeTypes.Supplier nodeTypesSupplier,
NodeTypePredicate matcher ) {
// this returns null for backwards compatibility reasons (otherwise it should've been abstract)
return null;
}
@GuardedBy( "this" )
private void removeProvidedIndexes( ChangeBus observable,
Predicate<AtomicIndex> predicate ) {
Iterator<Map.Entry<String, Map<String, AtomicIndex>>> iter = providedIndexesByWorkspaceNameByIndexName.entrySet()
.iterator();
while (iter.hasNext()) {
Map<String, AtomicIndex> byWorkspaceName = iter.next().getValue();
if (byWorkspaceName.isEmpty()) continue;
Iterator<Map.Entry<String, AtomicIndex>> providedIter = byWorkspaceName.entrySet().iterator();
while (providedIter.hasNext()) {
AtomicIndex index = providedIter.next().getValue();
if (predicate.test(index)) {
removeProvidedIndex(index, observable);
providedIter.remove();
// Look for this provided index in the reverse lookup ...
Map<String, AtomicIndex> byIndexName = providedIndexesByIndexNameByWorkspaceName.get(index.workspaceName());
byIndexName.remove(index.getName());
}
}
}
}
@GuardedBy( "this" )
private void registerIndex( AtomicIndex index,
ChangeBus observable ) {
// Register the index as a listener will work even when clustered.
if (index.indexDefinition().isSynchronous()) {
// The index should be updated synchronously in the same thread that submits the events to the bus (before the
// 'notify' method returns), and the "in-thread" behavior is what does this ...
observable.registerInThread(index);
} else {
// The index is to be updated asynchronously, so use a normal listener ...
observable.register(index);
}
}
@GuardedBy( "this" )
private void removeProvidedIndex( AtomicIndex index,
ChangeBus observable ) {
try {
observable.unregister(index);
removeIndex(index.indexDefinition(), index.managed(), index.workspaceName());
} catch (RuntimeException e) {
String msg = "Error removing index '{0}' in workspace '{1}' with definition: {2}";
logger().error(e, msg, index.getName(), index.workspaceName(), index.indexDefinition());
}
}
private NodeTypeMatcher nodeTypePredicate( NodeTypes nodeTypes,
IndexDefinition defn ) {
// Get the indexed node type ...
String indexedNodeType = defn.getNodeTypeName();
Name indexedNodeTypeName = context().getValueFactories().getNameFactory().create(indexedNodeType);
Set<Name> allNodeTypes = nodeTypes.getAllSubtypes(indexedNodeTypeName);
assert allNodeTypes != null;
return NodeTypeMatcher.create(allNodeTypes, nodeTypes);
}
/**
* This class is used within IndexProvider to keep a thread-safe object for each index. Even when the IndexDefinition for that
* index is changed, the same instance will always associated with that definition/workspace pair. This is actually the
* {@link Index} implementation exposed by the {@link IndexProvider#getIndex(String, String)} method, though it largely
* delegates to the most current {@link ManagedIndex} instance created by the provider.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
@ThreadSafe
private final class AtomicIndex implements Index, ChangeSetListener {
private final String workspaceName;
private volatile ManagedIndex managedIndex;
private volatile IndexDefinition defn;
private final NodeTypeMatcher matcher;
protected AtomicIndex( IndexDefinition defn,
ManagedIndex managedIndex,
String workspaceName,
NodeTypeMatcher matcher ) {
this.defn = defn;
this.managedIndex = managedIndex;
this.workspaceName = workspaceName;
this.matcher = matcher;
}
protected final IndexDefinition indexDefinition() {
return defn;
}
protected final String workspaceName() {
return workspaceName;
}
@Override
public final String getProviderName() {
return IndexProvider.this.getName();
}
@Override
public final String getName() {
return defn.getName();
}
@Override
public boolean supportsFullTextConstraints() {
return defn.getKind() == IndexKind.TEXT;
}
@Override
public boolean isEnabled() {
return managedIndex.isEnabled();
}
@Override
public final Results filter(IndexConstraints constraints, long cardinalityEstimate) {
return managedIndex.filter(constraints, cardinalityEstimate);
}
@Override
public final void notify( ChangeSet changeSet ) {
if (changeSet.getWorkspaceName() != null) {
// This is a change in the content of a workspace ...
managedIndex.getIndexChangeAdapter().notify(changeSet);
}
}
protected ManagedIndex managed() {
return managedIndex;
}
public void shutdown( boolean destroyed ) {
managedIndex.shutdown(destroyed);
}
protected final void update( ManagedIndex managedIndex,
IndexDefinition newDefinition,
NodeTypeMatcher matcher ) {
this.managedIndex = managedIndex;
this.defn = newDefinition;
this.matcher.use(matcher);
}
}
}