/*
* 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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.jcr.RepositoryException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.collection.ArrayListMultimap;
import org.modeshape.common.collection.Collections;
import org.modeshape.common.collection.Multimap;
import org.modeshape.common.collection.Problems;
import org.modeshape.common.collection.SimpleProblems;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.util.Reflection;
import org.modeshape.jcr.RepositoryConfiguration.Component;
import org.modeshape.jcr.api.index.IndexColumnDefinition;
import org.modeshape.jcr.api.index.IndexColumnDefinitionTemplate;
import org.modeshape.jcr.api.index.IndexDefinition;
import org.modeshape.jcr.api.index.IndexDefinition.IndexKind;
import org.modeshape.jcr.api.index.IndexDefinitionTemplate;
import org.modeshape.jcr.api.index.IndexExistsException;
import org.modeshape.jcr.api.index.InvalidIndexDefinitionException;
import org.modeshape.jcr.api.index.NoSuchIndexException;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.cache.WorkspaceNotFoundException;
import org.modeshape.jcr.cache.change.Change;
import org.modeshape.jcr.cache.change.ChangeSet;
import org.modeshape.jcr.cache.change.NodeAdded;
import org.modeshape.jcr.cache.change.NodeRemoved;
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.query.CompositeIndexWriter;
import org.modeshape.jcr.spi.index.IndexDefinitionChanges;
import org.modeshape.jcr.spi.index.IndexFeedback;
import org.modeshape.jcr.spi.index.IndexFeedback.IndexingCallback;
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.spi.index.provider.IndexProvider;
import org.modeshape.jcr.spi.index.provider.IndexProviderExistsException;
import org.modeshape.jcr.spi.index.provider.ManagedIndex;
import org.modeshape.jcr.spi.index.provider.NoSuchProviderException;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.StringFactory;
import org.modeshape.jcr.value.ValueFactory;
/**
* The {@link RepositoryIndexManager} is the maintainer of index definitions for the entire repository at run-time. The repository
* index manager maintains an immutable view of all index definitions.
*/
@ThreadSafe
class RepositoryIndexManager implements IndexManager, NodeTypes.Listener {
/**
* Names of properties that are known to have non-unique values when used in a single-valued index.
*/
private static final Set<Name> NON_UNIQUE_PROPERTY_NAMES = Collections.unmodifiableSet(JcrLexicon.PRIMARY_TYPE,
JcrLexicon.MIXIN_TYPES,
JcrLexicon.PATH,
ModeShapeLexicon.DEPTH,
ModeShapeLexicon.LOCALNAME);
/**
* Names of properties that are known to have non-enumerated values when used in a single-valued index.
*/
private static final Set<Name> NON_ENUMERATED_PROPERTY_NAMES = Collections.unmodifiableSet(JcrLexicon.PRIMARY_TYPE,
JcrLexicon.MIXIN_TYPES,
JcrLexicon.PATH,
ModeShapeLexicon.DEPTH,
ModeShapeLexicon.LOCALNAME);
private final JcrRepository.RunningState repository;
private final RepositoryConfiguration config;
private final ExecutionContext context;
private final String systemWorkspaceName;
private final Path indexesPath;
private final Collection<Component> components;
private final ConcurrentMap<String, IndexProvider> providers = new ConcurrentHashMap<>();
private final AtomicBoolean initialized = new AtomicBoolean(false);
private volatile IndexWriter indexWriter;
private final Logger logger = Logger.getLogger(getClass());
private volatile RepositoryIndexes indexes = RepositoryIndexes.NO_INDEXES;
RepositoryIndexManager( JcrRepository.RunningState repository,
RepositoryConfiguration config ) {
this.repository = repository;
this.config = config;
this.context = repository.context();
this.systemWorkspaceName = this.repository.repositoryCache().getSystemWorkspaceName();
PathFactory pathFactory = this.context.getValueFactories().getPathFactory();
this.indexesPath = pathFactory.createAbsolutePath(JcrLexicon.SYSTEM, ModeShapeLexicon.INDEXES);
// Set up the index providers ...
this.components = config.getIndexProviders();
for (Component component : components) {
try {
IndexProvider provider = component.createInstance();
register(provider);
} catch (Throwable t) {
if (t.getCause() != null) {
t = t.getCause();
}
this.repository.error(t, JcrI18n.unableToInitializeIndexProvider, component, repository.name(), t.getMessage());
}
}
}
/**
* Initialize this manager by calling {@link IndexProvider#initialize()} on each of the currently-registered providers.
*
* @return the information about the portions of the repository that need to be scanned to (re)build indexes; null if no
* scanning is required
*/
protected synchronized ScanningTasks initialize() {
if (initialized.get()) {
// nothing to do ...
return null;
}
// Initialize each of the providers, removing any that are not properly initialized ...
for (Iterator<Map.Entry<String, IndexProvider>> providerIter = providers.entrySet().iterator(); providerIter.hasNext();) {
IndexProvider provider = providerIter.next().getValue();
try {
doInitialize(provider);
} catch (Throwable t) {
if (t.getCause() != null) {
t = t.getCause();
}
repository.error(t, JcrI18n.unableToInitializeIndexProvider, provider.getName(), repository.name(),
t.getMessage());
providerIter.remove();
}
}
// Re-read the index definitions in case there were disabled index definitions that used the now-available provider ...
RepositoryIndexes indexes = readIndexDefinitions();
// Notify the providers of all the index definitions (which we'll treat as "new" since we're just starting up) ...
ScanningTasks feedback = new ScanningTasks();
for (Iterator<Map.Entry<String, IndexProvider>> providerIter = providers.entrySet().iterator(); providerIter.hasNext();) {
IndexProvider provider = providerIter.next().getValue();
if (provider == null) continue;
final String providerName = provider.getName();
IndexChanges changes = new IndexChanges();
for (IndexDefinition indexDefn : indexes.getIndexDefinitions().values()) {
if (!providerName.equals(indexDefn.getProviderName())) continue;
changes.change(indexDefn);
}
// Even if there are no definitions, we still want to notify each of the providers ...
try {
provider.notify(changes, repository.changeBus(), repository.nodeTypeManager(), repository.repositoryCache()
.getWorkspaceNames(),
feedback.forProvider(providerName));
} catch (RuntimeException e) {
logger.error(e, JcrI18n.errorNotifyingProviderOfIndexChanges, providerName, repository.name(), e.getMessage());
}
}
// Refresh the index writer ...
refreshIndexWriter();
initialized.set(true);
return feedback;
}
@Override
public void notify( NodeTypes updatedNodeTypes ) {
// Notify all of the providers about the change in node types ...
for (IndexProvider provider : providers.values()) {
provider.notify(updatedNodeTypes);
}
}
protected synchronized void importIndexDefinitions() throws RepositoryException {
RepositoryConfiguration.Indexes indexes = config.getIndexes();
if (indexes.isEmpty()) return;
List<IndexDefinition> defns = indexes.getIndexNames().stream()
.map(indexes::getIndex)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!defns.isEmpty()) {
IndexDefinition[] array = defns.toArray(new IndexDefinition[defns.size()]);
registerIndexes(array, true);
}
}
protected void refreshIndexWriter() {
indexWriter = CompositeIndexWriter.create(providers.values());
}
/**
* Initialize the supplied provider.
*
* @param provider the provider; may not be null
* @throws RepositoryException if there is a problem initializing the provider
*/
protected void doInitialize( IndexProvider provider ) throws RepositoryException {
// Set the execution context instance ...
Reflection.setValue(provider, "context", repository.context());
// Set the environment
Reflection.setValue(provider, "environment", repository.environment());
provider.initialize();
// If successful, call the 'postInitialize' method reflectively (due to inability to call directly) ...
Method postInitialize = Reflection.findMethod(IndexProvider.class, "postInitialize");
Reflection.invokeAccessibly(provider, postInitialize, new Object[] {});
if (logger.isDebugEnabled()) {
logger.debug("Successfully initialized index provider '{0}' in repository '{1}'", provider.getName(),
repository.name());
}
}
void shutdown() {
for (IndexProvider provider : providers.values()) {
try {
provider.shutdown();
} catch (RepositoryException e) {
logger.error(e, JcrI18n.errorShuttingDownIndexProvider, repository.name(), provider.getName(), e.getMessage());
}
}
}
/**
* Get the query index writer that will delegate to all registered providers.
*
* @return the query index writer instance; never null
*/
IndexWriter getIndexWriter() {
return indexWriter;
}
/**
* Get the query index writer that will delegate to only those registered providers with the given names.
*
* @param providerNames the names of the providers that require indexing
* @return a query index writer instance; never null
*/
IndexWriter getIndexWriterForProviders( Set<String> providerNames ) {
List<IndexProvider> reindexProviders = new LinkedList<>();
for (IndexProvider provider : providers.values()) {
if (providerNames.contains(provider.getName())) {
reindexProviders.add(provider);
}
}
return CompositeIndexWriter.create(reindexProviders);
}
@Override
public synchronized void register( IndexProvider provider ) throws RepositoryException {
if (providers.containsKey(provider.getName())) {
throw new IndexProviderExistsException(JcrI18n.indexProviderAlreadyExists.text(provider.getName(), repository.name()));
}
// Set the repository name field ...
Reflection.setValue(provider, "repositoryName", repository.name());
// Set the logger instance
Reflection.setValue(provider, "logger", ExtensionLogger.getLogger(provider.getClass()));
// Set the name of the system workspace
Reflection.setValue(provider, "systemWorkspaceName", systemWorkspaceName);
if (initialized.get()) {
// This manager is already initialized, so we have to initialize the new provider ...
doInitialize(provider);
}
// Do this last so that it doesn't show up in the list of providers before it's properly initialized ...
IndexProvider existing = providers.putIfAbsent(provider.getName(), provider);
if (existing != null) {
throw new IndexProviderExistsException(JcrI18n.indexProviderAlreadyExists.text(provider.getName(), repository.name()));
}
// Re-read the index definitions in case there were disabled index definitions that used the now-available provider ...
readIndexDefinitions();
// Refresh the index writer ...
refreshIndexWriter();
}
@Override
public void unregister( String providerName ) throws RepositoryException {
IndexProvider provider = providers.remove(providerName);
if (provider == null) {
throw new NoSuchProviderException(JcrI18n.indexProviderDoesNotExist.text(providerName, repository.name()));
}
if (initialized.get()) {
provider.shutdown();
}
// Re-read the index definitions in case there were disabled index definitions that used the now-available provider ...
readIndexDefinitions();
// Refresh the index writer ...
refreshIndexWriter();
}
@Override
public IndexStatus getIndexStatus( String providerName, String indexName, String workspaceName ) {
CheckArg.isNotNull(providerName, "providerName");
CheckArg.isNotNull(indexName, "indexName");
CheckArg.isNotNull(workspaceName, "workspaceName");
IndexProvider provider = getProvider(providerName);
if (provider == null) {
return IndexStatus.NON_EXISTENT;
}
ManagedIndex managedIndex = provider.getManagedIndex(indexName, workspaceName);
return managedIndex != null ? managedIndex.getStatus() : IndexStatus.NON_EXISTENT;
}
@Override
public List<ManagedIndex> getIndexes(String providerName, String workspaceName, final IndexStatus status) {
CheckArg.isNotNull(providerName, "providerName");
CheckArg.isNotNull(workspaceName, "workspaceName");
final List<ManagedIndex> result = new ArrayList<>();
IndexProvider provider = getProvider(providerName);
if (provider == null) {
return result;
}
provider.onEachIndexInWorkspace(workspaceName, new IndexProvider.ManagedIndexOperation() {
@Override
public void apply( String workspaceName, ManagedIndex index, IndexDefinition defn ) {
if (index.getStatus().equals(status)) {
result.add(index);
}
}
});
return result;
}
@Override
public List<String> getIndexNames( String providerName, String workspaceName, final IndexStatus status ) {
CheckArg.isNotNull(providerName, "providerName");
CheckArg.isNotNull(workspaceName, "workspaceName");
final List<String> result = new ArrayList<>();
IndexProvider provider = getProvider(providerName);
if (provider == null) {
return result;
}
provider.onEachIndexInWorkspace(workspaceName, new IndexProvider.ManagedIndexOperation() {
@Override
public void apply( String workspaceName, ManagedIndex index, IndexDefinition defn ) {
if (index.getStatus().equals(status)) {
result.add(defn.getName());
}
}
});
return result;
}
@Override
public Set<String> getProviderNames() {
return Collections.unmodifiableSet(new HashSet<>(providers.keySet()));
}
protected Iterable<IndexProvider> getProviders() {
return new ArrayList<>(providers.values());
}
@Override
public IndexProvider getProvider( String name ) {
return providers.get(name);
}
@Override
public Map<String, IndexDefinition> getIndexDefinitions() {
return indexes.getIndexDefinitions();
}
@Override
public IndexColumnDefinitionTemplate createIndexColumnDefinitionTemplate() {
return new RepositoryIndexColumnDefinitionTemplate();
}
@Override
public IndexDefinitionTemplate createIndexDefinitionTemplate() {
return new RepositoryIndexDefinitionTemplate();
}
@Override
public void registerIndex( IndexDefinition indexDefinition,
boolean allowUpdate )
throws InvalidIndexDefinitionException, IndexExistsException, RepositoryException {
registerIndexes(new IndexDefinition[] {indexDefinition}, allowUpdate);
}
@Override
public void registerIndexes( IndexDefinition[] indexDefinitions,
boolean allowUpdate ) throws InvalidIndexDefinitionException, IndexExistsException {
CheckArg.isNotNull(indexDefinitions, "indexDefinitions");
// Before we do anything, validate each of the index definitions and throw an exception ...
RepositoryNodeTypeManager nodeTypeManager = repository.nodeTypeManager();
List<IndexDefinition> validated = new ArrayList<>(indexDefinitions.length);
Problems problems = new SimpleProblems();
for (IndexDefinition defn : indexDefinitions) {
String name = defn.getName();
String providerName = defn.getProviderName();
if (name == null) {
problems.addError(JcrI18n.indexMustHaveName, defn, repository.name());
continue;
}
if (indexes.getIndexDefinitions().containsKey(name) && !allowUpdate) {
// Throw this one immediately ...
String msg = JcrI18n.indexAlreadyExists.text(defn.getName(), repository.name());
throw new IndexExistsException(msg);
}
if (providerName == null) {
problems.addError(JcrI18n.indexMustHaveProviderName, defn.getName(), repository.name());
continue;
}
if (defn.hasSingleColumn()) {
IndexColumnDefinition columnDefn = defn.getColumnDefinition(0);
Name propName = context.getValueFactories().getNameFactory().create(columnDefn.getPropertyName());
switch (defn.getKind()) {
case UNIQUE_VALUE:
if (NON_UNIQUE_PROPERTY_NAMES.contains(propName)) {
problems.addError(JcrI18n.unableToCreateUniqueIndexForColumn, defn.getName(),
columnDefn.getPropertyName());
}
break;
case ENUMERATED_VALUE:
if (NON_ENUMERATED_PROPERTY_NAMES.contains(propName)) {
problems.addError(JcrI18n.unableToCreateEnumeratedIndexForColumn, defn.getName(),
columnDefn.getPropertyName());
}
break;
case VALUE:
case NODE_TYPE:
case TEXT:
break;
}
} else {
// Multiple columns ...
if (defn.getKind() == IndexKind.NODE_TYPE) {
// must be single-column indexes
problems.addError(JcrI18n.nodeTypeIndexMustHaveOneColumn, defn.getName());
}
}
IndexProvider provider = providers.get(providerName);
if (provider == null) {
problems.addError(JcrI18n.indexProviderDoesNotExist, defn.getName(), repository.name());
} else {
// Perform some default validations that should be applied to all providers...
provider.validateDefaultColumnTypes(context, defn, problems);
// Then have the provider perform any custom validations
provider.validateProposedIndex(context, defn, nodeTypeManager, problems);
// Create an instance of our own definition implementation ...
defn = RepositoryIndexDefinition.createFrom(defn, true);
validated.add(defn);
}
}
if (problems.hasErrors()) {
String msg = JcrI18n.invalidIndexDefinitions.text(repository.name(), problems);
throw new InvalidIndexDefinitionException(new JcrProblems(problems), msg);
}
SessionCache systemCache = repository.createSystemSession(context, false);
SystemContent system = new SystemContent(systemCache);
for (IndexDefinition defn : validated) {
String providerName = defn.getProviderName();
// Determine if the index should be enabled ...
defn = RepositoryIndexDefinition.createFrom(defn, providers.containsKey(providerName));
// Write the definition to the system area ...
system.store(defn, allowUpdate);
}
// Save the changes ...
systemCache.save();
// Refresh the immutable snapshot ...
this.indexes = readIndexDefinitions();
}
@Override
public void unregisterIndexes( String... indexNames ) throws NoSuchIndexException, RepositoryException {
if (indexNames == null || indexNames.length == 0) return;
// Remove the definition from the system area ...
SessionCache systemCache = repository.createSystemSession(context, false);
SystemContent system = new SystemContent(systemCache);
for (String indexName : indexNames) {
IndexDefinition defn = indexes.getIndexDefinitions().get(indexName);
if (defn == null) {
throw new NoSuchIndexException(JcrI18n.indexDoesNotExist.text(indexName, repository.name()));
}
system.remove(defn);
}
system.save();
// Refresh the immutable snapshot ...
this.indexes = readIndexDefinitions();
}
RepositoryIndexManager with( JcrRepository.RunningState repository ) {
return new RepositoryIndexManager(repository, config);
}
protected final ValueFactory<String> strings() {
return this.context.getValueFactories().getStringFactory();
}
/**
* Get an immutable snapshot of the index definitions. This can be used by the query engine to determine which indexes might
* be usable when querying a specific selector (node type).
*
* @return a snapshot of the index definitions at this moment; never null
*/
public RepositoryIndexes getIndexes() {
return indexes;
}
protected ScanningTasks notify( ChangeSet changeSet ) {
if (changeSet.getWorkspaceName() == null) {
// This is a change to the workspaces or repository metadata ...
// Refresh the index definitions ...
RepositoryIndexes indexes = readIndexDefinitions();
ScanningTasks feedback = new ScanningTasks();
if (!indexes.getIndexDefinitions().isEmpty()) {
// Build up the names of the added and removed workspace names ...
Set<String> addedWorkspaces = new HashSet<>();
Set<String> removedWorkspaces = new HashSet<>();
for (Change change : changeSet) {
if (change instanceof WorkspaceAdded) {
WorkspaceAdded added = (WorkspaceAdded)change;
addedWorkspaces.add(added.getWorkspaceName());
} else if (change instanceof WorkspaceRemoved) {
WorkspaceRemoved removed = (WorkspaceRemoved)change;
removedWorkspaces.add(removed.getWorkspaceName());
}
}
if (!addedWorkspaces.isEmpty() || !removedWorkspaces.isEmpty()) {
// Figure out which providers need to be called, and which definitions go with those providers ...
Map<String, List<IndexDefinition>> defnsByProvider = new HashMap<>();
for (IndexDefinition defn : indexes.getIndexDefinitions().values()) {
String providerName = defn.getProviderName();
List<IndexDefinition> defns = defnsByProvider.get(providerName);
if (defns == null) {
defns = new ArrayList<>();
defnsByProvider.put(providerName, defns);
}
defns.add(defn);
}
// Then for each provider ...
for (Map.Entry<String, List<IndexDefinition>> entry : defnsByProvider.entrySet()) {
String providerName = entry.getKey();
WorkspaceIndexChanges changes = new WorkspaceIndexChanges(entry.getValue(), addedWorkspaces,
removedWorkspaces);
IndexProvider provider = providers.get(providerName);
if (provider == null) continue;
provider.notify(changes, repository.changeBus(), repository.nodeTypeManager(),
repository.repositoryCache().getWorkspaceNames(), feedback.forProvider(providerName));
}
}
}
return feedback;
}
if (!systemWorkspaceName.equals(changeSet.getWorkspaceName())) {
// The change does not affect the 'system' workspace, so skip it ...
return null;
}
// It is simple to listen to all local and remote changes. Therefore, any changes made locally to the index definitions
// will be propagated through the cached representation via this listener.
AtomicReference<Map<Name, IndexChangeInfo>> changesByProviderName = new AtomicReference<>();
for (Change change : changeSet) {
if (change instanceof NodeAdded) {
NodeAdded added = (NodeAdded)change;
Path addedPath = added.getPath();
if (indexesPath.isAncestorOf(addedPath)) {
// Get the name of the affected provider ...
Name providerName = addedPath.getSegment(2).getName();
if (addedPath.size() > 3) {
// Adding an index (or column definition), but all we care about is the name of the index
Name indexName = addedPath.getSegment(3).getName();
changeInfoForProvider(changesByProviderName, providerName).changed(indexName);
}
}
} else if (change instanceof NodeRemoved) {
NodeRemoved removed = (NodeRemoved)change;
Path removedPath = removed.getPath();
if (indexesPath.isAncestorOf(removedPath)) {
// Get the name of the affected provider ...
Name providerName = removedPath.getSegment(2).getName();
if (removedPath.size() > 4) {
// It's a column definition being removed, so the index is changed ...
Name indexName = removedPath.getSegment(3).getName();
changeInfoForProvider(changesByProviderName, providerName).removed(indexName);
} else if (removedPath.size() > 3) {
// Removing an index (or column definition), but all we care about is the name of the index
Name indexName = removedPath.getSegment(3).getName();
changeInfoForProvider(changesByProviderName, providerName).removed(indexName);
} else if (removedPath.size() == 3) {
// The whole provider was removed ...
changeInfoForProvider(changesByProviderName, providerName).removedAll();
}
}
} else if (change instanceof PropertyChanged) {
PropertyChanged propChanged = (PropertyChanged)change;
Path changedPath = propChanged.getPathToNode();
if (indexesPath.isAncestorOf(changedPath)) {
if (changedPath.size() > 3) {
// Adding an index (or column definition), but all we care about is the name of the index
Name providerName = changedPath.getSegment(2).getName();
Name indexName = changedPath.getSegment(3).getName();
changeInfoForProvider(changesByProviderName, providerName).changed(indexName);
}
}
} // we don't care about node moves (don't happen) or property added/removed (handled by node add/remove)
}
if (changesByProviderName.get() == null || changesByProviderName.get().isEmpty()) {
// No changes to the indexes ...
return null;
}
// Refresh the index definitions ...
RepositoryIndexes indexes = readIndexDefinitions();
// And notify the affected providers ...
StringFactory strings = context.getValueFactories().getStringFactory();
ScanningTasks feedback = new ScanningTasks();
for (Map.Entry<Name, IndexChangeInfo> entry : changesByProviderName.get().entrySet()) {
String providerName = strings.create(entry.getKey());
IndexProvider provider = providers.get(providerName);
if (provider == null) continue;
IndexChanges changes = new IndexChanges();
IndexChangeInfo info = entry.getValue();
if (info.removedAll) {
// Get all of the definitions for this provider ...
for (IndexDefinition defn : indexes.getIndexDefinitions().values()) {
if (defn.getProviderName().equals(providerName)) changes.remove(defn.getName());
}
}
// Others might have been added or changed after the existing ones were removed ...
for (Name name : info.removedIndexes) {
changes.remove(strings.create(name));
}
for (Name name : info.changedIndexes) {
IndexDefinition defn = indexes.getIndexDefinitions().get(strings.create(name));
if (defn != null) changes.change(defn);
}
// Notify the provider ...
try {
provider.notify(changes, repository.changeBus(), repository.nodeTypeManager(), repository.repositoryCache()
.getWorkspaceNames(),
feedback.forProvider(providerName));
} catch (RuntimeException e) {
logger.error(e, JcrI18n.errorNotifyingProviderOfIndexChanges, providerName, repository.name(), e.getMessage());
}
}
// Finally swap the snapshot of indexes ...
this.indexes = indexes;
return feedback;
}
protected boolean hasProviders() {
return !providers.isEmpty();
}
protected static IndexChangeInfo changeInfoForProvider( AtomicReference<Map<Name, IndexChangeInfo>> changesByProviderName,
Name providerName ) {
Map<Name, IndexChangeInfo> byProviderName = changesByProviderName.get();
if (byProviderName == null) {
byProviderName = new HashMap<>();
changesByProviderName.set(byProviderName);
}
IndexChangeInfo info = byProviderName.get(providerName);
if (info == null) {
info = new IndexChangeInfo();
byProviderName.put(providerName, info);
}
return info;
}
protected static final class IndexChangeInfo {
protected final Set<Name> changedIndexes = new HashSet<>();
protected final Set<Name> removedIndexes = new HashSet<>();
protected boolean removedAll = false;
public void changed( Name indexName ) {
changedIndexes.add(indexName);
removedIndexes.remove(indexName);
}
public void removed( Name indexName ) {
removedIndexes.add(indexName);
changedIndexes.remove(indexName);
}
public void removedAll() {
removedAll = true;
removedIndexes.clear();
changedIndexes.clear();
}
}
protected static final class IndexChanges implements IndexDefinitionChanges {
private final Set<String> removedDefinitions = new HashSet<>();
private final Map<String, IndexDefinition> changedDefinitions = new HashMap<>();
protected void remove( String name ) {
removedDefinitions.add(name);
}
protected void change( IndexDefinition indexDefn ) {
this.changedDefinitions.put(indexDefn.getName(), indexDefn);
}
@Override
public Set<String> getRemovedIndexDefinitions() {
return removedDefinitions;
}
@Override
public Map<String, IndexDefinition> getUpdatedIndexDefinitions() {
return changedDefinitions;
}
}
protected static final class WorkspaceIndexChanges implements WorkspaceChanges {
private final List<IndexDefinition> definitions;
private final Set<String> addedWorkspaceNames;
private final Set<String> removedWorkspaceNames;
protected WorkspaceIndexChanges( List<IndexDefinition> defns,
Set<String> addedWorkspaces,
Set<String> removedWorkspaces ) {
this.definitions = defns;
this.addedWorkspaceNames = addedWorkspaces;
this.removedWorkspaceNames = removedWorkspaces;
}
@Override
public Collection<IndexDefinition> getIndexDefinitions() {
return definitions;
}
@Override
public Set<String> getAddedWorkspaces() {
return addedWorkspaceNames;
}
@Override
public Set<String> getRemovedWorkspaces() {
return removedWorkspaceNames;
}
}
protected RepositoryIndexes readIndexDefinitions() {
// There were at least some changes ...
NodeTypes nodeTypes = repository.nodeTypeManager().getNodeTypes();
try {
// Read the affected index definitions ...
SessionCache systemCache = repository.createSystemSession(context, false);
SystemContent system = new SystemContent(systemCache);
Collection<IndexDefinition> indexDefns = system.readAllIndexDefinitions(providers.keySet());
this.indexes = new Indexes(context, indexDefns, nodeTypes);
return this.indexes;
} catch (WorkspaceNotFoundException e) {
// This happens occasionally when shutting down ...
} catch (Throwable e) {
logger.error(e, JcrI18n.errorRefreshingIndexDefinitions, repository.name());
}
return indexes;
}
/**
* An immutable view of the indexes defined for the repository.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
@Immutable
public static final class Indexes extends RepositoryIndexes {
private final Map<String, IndexDefinition> indexByName = new HashMap<>();
private final Map<String, Map<String, Collection<IndexDefinition>>> indexesByProviderByNodeTypeName = new HashMap<>();
protected Indexes( ExecutionContext context,
Collection<IndexDefinition> defns,
NodeTypes nodeTypes ) {
// Identify the subtypes for each node type, and do this before we build any views ...
if (!defns.isEmpty()) {
Map<Name, Collection<String>> subtypesByName = new HashMap<>();
for (JcrNodeType nodeType : nodeTypes.getAllNodeTypes()) {
// For each of the supertypes ...
for (JcrNodeType supertype : nodeType.getTypeAndSupertypes()) {
Collection<String> types = subtypesByName.get(supertype.getInternalName());
if (types == null) {
types = new LinkedList<>();
subtypesByName.put(supertype.getInternalName(), types);
}
types.add(nodeType.getName());
}
}
// Now process all of the indexes ...
NameFactory names = context.getValueFactories().getNameFactory();
for (IndexDefinition defn : defns) {
Name nodeTypeName = names.create(defn.getNodeTypeName());
if (!subtypesByName.containsKey(nodeTypeName)){
Logger.getLogger(getClass()).warn(JcrI18n.errorIndexing,"not creating index "+ defn.getName() +" because of unknown nodeType " + nodeTypeName.getString());
continue;
}
indexByName.put(defn.getName(), defn);
// Now find out all of the node types that are or subtype the named node types ...
for (String typeAndSubtype : subtypesByName.get(nodeTypeName)) {
Map<String, Collection<IndexDefinition>> byProvider = indexesByProviderByNodeTypeName.get(typeAndSubtype);
if (byProvider == null) {
byProvider = new HashMap<>();
indexesByProviderByNodeTypeName.put(typeAndSubtype, byProvider);
}
Collection<IndexDefinition> indexes = byProvider.get(defn.getProviderName());
if (indexes == null) {
indexes = new LinkedList<>();
byProvider.put(typeAndSubtype, indexes);
}
indexes.add(defn);
}
}
}
}
@Override
public boolean hasIndexDefinitions() {
return !indexByName.isEmpty();
}
@Override
public Map<String, IndexDefinition> getIndexDefinitions() {
return java.util.Collections.unmodifiableMap(indexByName);
}
@Override
public Iterable<IndexDefinition> indexesFor( String nodeTypeName,
String providerName ) {
Map<String, Collection<IndexDefinition>> defnsByProvider = indexesByProviderByNodeTypeName.get(nodeTypeName);
if (defnsByProvider == null) return null;
return defnsByProvider.get(providerName);
}
}
static interface ScanOperation {
public void scan(String workspace, Path path, IndexWriter writer );
}
/**
* An immutable set of provider names and non-overlapping workspace-path pairs.
*
* @author Randall Hauch (rhauch@redhat.com)
*/
@Immutable
static class ScanningRequest {
protected static final ScanningRequest EMPTY = new ScanningRequest();
private final Set<String> providerNames;
private final Multimap<String, PathToScan> pathsToScanByWorkspace;
protected ScanningRequest() {
this.providerNames = java.util.Collections.emptySet();
this.pathsToScanByWorkspace = ArrayListMultimap.create();
}
protected ScanningRequest( Multimap<String, PathToScan> pathsToScanByWorkspace,
Set<String> providerNames ) {
assert pathsToScanByWorkspace != null;
assert providerNames != null;
this.providerNames = Collections.unmodifiableSet(providerNames);
this.pathsToScanByWorkspace = pathsToScanByWorkspace;
}
/**
* Determine if this has no providers or workspace-path pairs.
*
* @return true if this request is empty, or false otherwise
*/
public boolean isEmpty() {
return providerNames.isEmpty();
}
/**
* Scan each required path in each of the workspaces.
*
* @param operation the scanning operation that is to be called for each workspace & path combination; may not be null
*/
public void onEachPathInWorkspace( ScanOperation operation ) {
for (Map.Entry<String, PathToScan> entry : pathsToScanByWorkspace.entries()) {
String workspaceName = entry.getKey();
PathToScan pathToScan = entry.getValue();
for (IndexingCallback callback : pathToScan) {
callback.beforeIndexing();
try {
operation.scan(workspaceName, pathToScan.path(), callback.writer());
} catch (Exception e) {
Logger.getLogger(getClass()).error(e, JcrI18n.errorIndexing, pathToScan.path(), workspaceName,
e.getMessage());
} finally {
callback.afterIndexing();
}
}
}
}
/**
* Get the set of provider names that are to be included in the scanning.
*
* @return the provider names; never null but possibly empty if {@link #isEmpty()} returns true
*/
public Set<String> providerNames() {
return providerNames;
}
}
private static class PathToScan implements Iterable<IndexingCallback> {
private final Path path;
private final Set<IndexingCallback> callbacks = new CopyOnWriteArraySet<>();
protected PathToScan( Path path,
IndexingCallback callback ) {
this.path = path;
if (callback != null) this.callbacks.add(callback);
}
public void addCallbacks( PathToScan other ) {
callbacks.addAll(other.callbacks);
}
public Path path() {
return path;
}
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals( Object obj ) {
return path.equals(obj);
}
@Override
public Iterator<IndexingCallback> iterator() {
return callbacks.iterator();
}
}
/**
* Threadsafe utility class for maintaining the list of providers and workspace-path pairs that need to be scanned. Instances
* can be safely combined using {@link #add(ScanningTasks)}, and immutable snapshots of the information can be obtained via
* {@link #drain()} (which atomically empties the providers and workspace-path pairs into the immutable
* {@link ScanningRequest}).
*
* @author Randall Hauch (rhauch@redhat.com)
*/
@ThreadSafe
static class ScanningTasks {
private final Set<String> providerNames = new HashSet<>();
private Multimap<String, PathToScan> pathsByWorkspaceName = ArrayListMultimap.create();
/**
* Add all of the provider names and workspace-path pairs from the supplied scanning task.
*
* @param other the other scanning task; may be null
* @return true if there is at least one workspace-path pair and provider, or false if there are none
*/
public synchronized boolean add( ScanningTasks other ) {
if (other != null) {
this.providerNames.addAll(other.providerNames);
for (Map.Entry<String, PathToScan> entry : other.pathsByWorkspaceName.entries()) {
add(entry.getKey(), entry.getValue());
}
}
return !this.providerNames.isEmpty();
}
/**
* Atomically drain all of the provider names and workspace-path pairs from this object and return them in an immutable
* {@link ScanningRequest}.
*
* @return the immutable set of provider names and workspace-path pairs; never null
*/
public synchronized ScanningRequest drain() {
if (this.providerNames.isEmpty()) return ScanningRequest.EMPTY;
Set<String> providerNames = new HashSet<>(this.providerNames);
Multimap<String, PathToScan> pathsToScanByWorkspace = this.pathsByWorkspaceName;
this.pathsByWorkspaceName = ArrayListMultimap.create();
this.providerNames.clear();
return new ScanningRequest(pathsToScanByWorkspace, providerNames);
}
protected synchronized void add( String providerName,
String workspaceName,
Path path,
IndexingCallback callback ) {
assert providerName != null;
assert workspaceName != null;
assert path != null;
providerNames.add(providerName);
add(workspaceName, path, callback);
}
private void add( String workspaceName,
PathToScan pathToScan ) {
Collection<PathToScan> pathsToScan = pathsByWorkspaceName.get(workspaceName);
if (pathsToScan.isEmpty()) {
pathsToScan.add(pathToScan);
} else {
Iterator<PathToScan> iter = pathsToScan.iterator();
boolean add = true;
final Path path = pathToScan.path();
while (iter.hasNext()) {
PathToScan existing = iter.next();
Path existingPath = existing.path();
if (path.isAtOrAbove(existingPath)) {
// Remove all of the existing paths that are at or above this path (we'll add it back in ...)
iter.remove();
// But add all of the callbacks ...
pathToScan.addCallbacks(existing);
} else if (path.isDescendantOf(existingPath)) {
// The new path is a descendant of an existing path, so we can stop now and do nothing ...
add = false;
// But add all of the callbacks ...
existing.addCallbacks(pathToScan);
break;
}
}
if (add) pathsByWorkspaceName.put(workspaceName, pathToScan);
}
}
private void add( String workspaceName,
Path path,
IndexingCallback callback ) {
add(workspaceName, new PathToScan(path, callback));
}
/**
* Obtain an {@link IndexFeedback} instance that can be used to gather feedback from the named provider.
*
* @param providerName the name of the index provider; may not be null
* @return the custom IndexFeedback instance; never null
*/
protected IndexFeedback forProvider( final String providerName ) {
assert providerName != null;
return new IndexFeedback() {
@Override
public void scan( String workspaceName,
IndexingCallback callback ) {
add(providerName, workspaceName, Path.ROOT_PATH, callback);
}
@Override
public void scan( String workspaceName,
IndexingCallback callback,
Path path ) {
add(providerName, workspaceName, path, callback);
}
};
}
}
}