/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*
* $Id: IndexManager.java 571354 2007-08-31 01:49:58Z natalia $
*/
package org.apache.xindice.core.indexer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xindice.Stopwatch;
import org.apache.xindice.core.Collection;
import org.apache.xindice.core.DBException;
import org.apache.xindice.core.data.Entry;
import org.apache.xindice.core.data.Key;
import org.apache.xindice.core.data.RecordSet;
import org.apache.xindice.util.Configuration;
import org.apache.xindice.util.ConfigurationCallback;
import org.apache.xindice.util.SimpleConfigurable;
import org.apache.xindice.util.XindiceException;
import org.apache.xindice.xml.SymbolTable;
import org.w3c.dom.Document;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.WeakHashMap;
/**
* IndexManager is a class that manages Indexes. Good description, eh?
* I should win a Pulitzer Prize for that one.
*
* @version $Revision: 571354 $, $Date: 2007-08-30 21:49:58 -0400 (Thu, 30 Aug 2007) $
*/
public final class IndexManager extends SimpleConfigurable {
private static final Log log = LogFactory.getLog(IndexManager.class);
private static final String[] EMPTY_STRINGS = new String[0];
private static final Indexer[] EMPTY_INDEXERS = new Indexer[0];
private static final String INDEX = "index";
private static final String NAME = "name";
private static final String CLASS = "class";
private static final Integer STATUS_READY = new Integer(0);
private static final Integer STATUS_BUSY = new Integer(1);
private Map indexes = new HashMap(); // Name to Indexer
private Map status = new HashMap(); // Name to Status
private Map bestIndexers = new HashMap(); // String to Map of IndexPattern to Indexer
private Collection collection;
private Timer timer;
private SymbolTable symbols;
private final List newIndexers = new ArrayList(); // of Indexers
private int taskCount; // counter of scheduled tasks
private final Object lock = new Object(); // lock object for manipulating taskCounter
/**
* Create IndexManager for a given collection
*
* @param collection Collection for this IndexManager
* @param timer Timer for indexing tasks
*/
public IndexManager(Collection collection, Timer timer) {
this.collection = collection;
this.symbols = collection.getSymbols();
this.timer = timer;
}
/**
* Configure index manager, register all indexes specified in the configuration
*
* @param config IndexManager configuration
*/
public void setConfig(Configuration config) throws XindiceException {
super.setConfig(config);
config.processChildren(INDEX, new ConfigurationCallback() {
public void process(Configuration cfg) {
String className = cfg.getAttribute(CLASS);
try {
register(Class.forName(className), cfg);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Failed to register index with class '" + className + "' for collection '" + collection.getCanonicalName() + "'", e);
}
}
}
});
}
/**
* list returns a list of the Indexers that this IndexerManager has
* registered.
*
* @return An array containing the Indexer names
*/
public synchronized String[] list() {
return (String[]) indexes.keySet().toArray(EMPTY_STRINGS);
}
/**
* drop physically removes the specified Indexer and any
* associated system resources that the Indexer uses.
*
* @param name The Indexer to drop
* @return Whether or not the Indexer was dropped
*/
public synchronized boolean drop(final String name) {
// Get indexer
Indexer idx = get(name);
// Unregister and remove from coniguration
unregister(name);
config.processChildren(INDEX, new ConfigurationCallback() {
public void process(Configuration cfg) {
try {
if (cfg.getAttribute(NAME).equals(name)) {
cfg.delete();
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
});
// Drop indexer
boolean res = false;
try {
res = idx.drop();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
return res;
}
/**
* Drop all indexers
*/
public synchronized void drop() {
String[] names = (String[]) indexes.keySet().toArray(new String[0]);
// Drop indexes
for (int i = 0; i < names.length; i++) {
drop(names[i]);
}
}
/**
* create creates a new Indexer object and any associated
* system resources that the Indexer will need.
*
* @param cfg The Indexer's configuration
* @return The Indexer that was created
* @throws DBException if unable to create specified indexer
*/
public synchronized Indexer create(Configuration cfg) throws DBException {
if (!INDEX.equals(cfg.getName())) {
throw new CannotCreateException("Cannot create index in " + collection.getCanonicalName() +
". Index configuration top element must be 'index'");
}
String name = cfg.getAttribute(NAME);
try {
// Check for duplicates
Configuration[] cfgs = config.getChildren();
for (int i = 0; i < cfgs.length; i++) {
if (cfgs[i].getAttribute(NAME).equals(name)) {
throw new DuplicateIndexException("Duplicate Index '" + name + "' in collection '" + collection.getCanonicalName() + "'");
}
}
String className = cfg.getAttribute(CLASS);
Indexer idx = register(Class.forName(className), cfg);
config.add(cfg);
return idx;
} catch (DBException e) {
throw e;
} catch (Exception e) {
throw new CannotCreateException("Cannot create index '" + name + "' in " + collection.getCanonicalName(), e);
}
}
/**
* Closes all indexers managed by this index manager.
*/
public synchronized void close() {
// wait for all scheduled tasks to finish
synchronized (lock) {
while (taskCount > 0) {
try {
lock.wait();
} catch (InterruptedException e) {
// ignore
}
}
}
// close all indexers
for (Iterator i = indexes.values().iterator(); i.hasNext(); ) {
Indexer idx = (Indexer) i.next();
try {
idx.close();
} catch (DBException e) {
if (log.isWarnEnabled()) {
log.warn("Failed to close indexer " + idx.getName() + " on collection " + collection.getCanonicalName(), e);
}
}
}
}
public synchronized Indexer register(Class c, Configuration cfg) throws DBException {
String name = null;
try {
Indexer idx = (Indexer) c.newInstance();
initialize(idx, cfg);
name = idx.getName();
if (name == null || name.trim().equals("")) {
throw new CannotCreateException("No name specified");
}
if (!idx.exists()) {
idx.create();
idx.open();
status.put(name, STATUS_BUSY);
synchronized (newIndexers) {
newIndexers.add(idx);
}
synchronized (lock) {
taskCount++;
try {
// Schedule new task
timer.schedule(new PopulateIndexersTimerTask(), 0);
} catch (RuntimeException e) {
// If failed to schedule the task, decrease the counter.
taskCount--;
throw e;
} catch (Error e) {
// If failed to schedule the task, decrease the counter.
taskCount--;
throw e;
}
if (log.isDebugEnabled()) {
log.debug("Scheduled new task, count is " + taskCount);
}
}
} else {
status.put(name, STATUS_READY);
idx.open();
}
indexes.put(name, idx);
Map tbl = (Map) bestIndexers.get(idx.getIndexStyle());
if (tbl != null) {
tbl.clear();
}
return idx;
} catch (DBException e) {
throw e;
} catch (Exception e) {
throw new CannotCreateException("Cannot create Index '" + name + "' in " + collection.getCanonicalName(), e);
}
}
public synchronized void unregister(String name) {
Indexer idx = (Indexer) indexes.remove(name);
status.remove(name);
String style = idx.getIndexStyle();
Map tbl = (Map) bestIndexers.get(style);
if (tbl != null) {
tbl.clear();
}
}
private void initialize(Indexer idx, Configuration cfg) throws XindiceException {
idx.setCollection(collection);
idx.setConfig(cfg);
}
private void populateNewIndexers() throws DBException {
Indexer[] list;
synchronized (newIndexers) {
list = (Indexer[]) newIndexers.toArray(EMPTY_INDEXERS);
newIndexers.clear();
}
if (list.length > 0) {
if (log.isTraceEnabled()) {
for (int i = 0; i < list.length; i++) {
log.trace("Index Creation: " + list[i].getName());
}
}
Stopwatch sw = new Stopwatch("Populated Indexes", true);
RecordSet rs = collection.getFiler().getRecordSet();
while (rs.hasMoreRecords()) {
// Read only key, we don't need filer-level value
Key key = rs.getNextKey();
Entry entry = collection.getEntry(key);
if (entry.getEntryType() == Entry.DOCUMENT) {
try {
new DocumentHandler(symbols, key, (Document) entry.getValue(), DocumentHandler.ACTION_CREATE, list);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Failed to index document " + key, e);
}
}
}
}
for (int i = 0; i < list.length; i++) {
try {
list[i].flush();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
status.put(list[i].getName(), STATUS_READY);
}
sw.stop();
if (log.isDebugEnabled()) {
for (int i = 0; i < list.length; i++) {
log.debug("Index Complete: " + list[i].getName());
}
log.debug(sw.toString());
}
}
}
/**
* get retrieves an Indexer by name.
*
* @param name The Indexer name
* @return The Indexer
*/
public synchronized Indexer get(String name) {
return (Indexer) indexes.get(name);
}
/**
* getBestIndexer retrieves the best Indexer to use for the specified
* IndexPattern.
*
* @param style The Indexer Style (ex: Node, Value)
* @param pattern The IndexPattern to use
* @return The best Indexer (or null)
*/
public Indexer getBestIndexer(String style, IndexPattern pattern) {
Map tbl = (Map) bestIndexers.get(style);
if (tbl == null) {
tbl = new WeakHashMap(); // FIXME: Review usage of WeakHashMap
bestIndexers.put(style, tbl);
}
Indexer idx = (Indexer) tbl.get(pattern);
if (idx == null) {
int highScore = 0;
Iterator i = indexes.values().iterator();
while (i.hasNext()) {
Indexer index = (Indexer) i.next();
// Indexer is not ready: can not use it
if (!status.get(index.getName()).equals(STATUS_READY)) {
continue;
}
// Indexer is of different style: can not use it
if (!index.getIndexStyle().equals(style)) {
continue;
}
// TODO: should it check patterns?
// there can be only one full text index for a collection
if (style.equals(Indexer.STYLE_FULLTEXT)) {
return index;
}
for (int j = 0; j < index.getPatterns().length; j++) {
int score = pattern.getMatchLevel(index.getPatterns()[j]);
if (score > highScore) {
idx = index;
highScore = score;
}
}
}
tbl.put(pattern, idx);
}
return idx;
}
public void addDocument(Key key, Document doc) {
if (indexes.size() > 0) {
Indexer[] idxList = (Indexer[]) indexes.values().toArray(EMPTY_INDEXERS);
new DocumentHandler(symbols, key, doc, DocumentHandler.ACTION_CREATE, idxList);
for (int i = 0; i < idxList.length; i++) {
try {
idxList[i].flush();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
}
}
public void removeDocument(Key key, Document doc) {
if (indexes.size() > 0) {
Indexer[] idxList = (Indexer[]) indexes.values().toArray(EMPTY_INDEXERS);
new DocumentHandler(symbols, key, doc, DocumentHandler.ACTION_DELETE, idxList);
for (int i = 0; i < idxList.length; i++) {
try {
idxList[i].flush();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
}
}
private class PopulateIndexersTimerTask extends TimerTask {
public void run() {
try {
populateNewIndexers();
} catch (DBException e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
} finally {
synchronized (lock) {
taskCount--;
if (log.isDebugEnabled()) {
log.debug("Task completed, count is " + taskCount);
}
if (taskCount == 0) {
lock.notifyAll();
}
}
}
}
}
}