/*
* Copyright 2004-2009 the original author or authors.
*
* 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.compass.core.lucene.engine.store;
import java.io.IOException;
import java.sql.Connection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.jdbc.JdbcDirectory;
import org.apache.lucene.store.jdbc.JdbcDirectorySettings;
import org.apache.lucene.store.jdbc.JdbcFileEntrySettings;
import org.apache.lucene.store.jdbc.JdbcStoreException;
import org.apache.lucene.store.jdbc.datasource.DataSourceUtils;
import org.apache.lucene.store.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.apache.lucene.store.jdbc.dialect.Dialect;
import org.apache.lucene.store.jdbc.dialect.DialectResolver;
import org.apache.lucene.store.jdbc.index.FetchPerTransactionJdbcIndexInput;
import org.apache.lucene.store.jdbc.support.JdbcTable;
import org.compass.core.CompassException;
import org.compass.core.config.CompassConfigurable;
import org.compass.core.config.CompassEnvironment;
import org.compass.core.config.CompassSettings;
import org.compass.core.config.ConfigurationException;
import org.compass.core.engine.SearchEngine;
import org.compass.core.engine.SearchEngineException;
import org.compass.core.engine.event.SearchEngineEventManager;
import org.compass.core.engine.event.SearchEngineLifecycleEventListener;
import org.compass.core.lucene.LuceneEnvironment;
import org.compass.core.lucene.engine.store.jdbc.DataSourceProvider;
import org.compass.core.lucene.engine.store.jdbc.DriverManagerDataSourceProvider;
import org.compass.core.util.ClassUtils;
/**
* @author kimchy
*/
public class JdbcDirectoryStore extends AbstractDirectoryStore implements CompassConfigurable {
private static final Log log = LogFactory.getLog(JdbcDirectoryStore.class);
public static final String PROTOCOL = "jdbc://";
private JdbcDirectorySettings jdbcSettings;
private DataSource dataSource;
private DataSourceProvider dataSourceProvider;
private Dialect dialect;
private boolean managed;
private boolean disableSchemaOperation;
private Map<String, JdbcTable> cachedJdbcTables = new ConcurrentHashMap<String, JdbcTable>();
/**
* Jdbc store should not support concurrent operations on the index within a single session.
*/
@Override
public boolean supportsConcurrentOperations() {
return false;
}
/**
* Jdbc store should not support concurrent operations on the index within a single session.
*/
@Override
public boolean supportsConcurrentCommits() {
return false;
}
public void configure(CompassSettings settings) throws CompassException {
String connection = settings.getSetting(CompassEnvironment.CONNECTION);
String url = connection.substring(PROTOCOL.length());
dataSourceProvider = (DataSourceProvider) settings.getSettingAsInstance(LuceneEnvironment.JdbcStore.DataSourceProvider.CLASS,
DriverManagerDataSourceProvider.class.getName());
dataSourceProvider.configure(url, settings);
this.dataSource = dataSourceProvider.getDataSource();
String dialectClassName = settings.getSetting(LuceneEnvironment.JdbcStore.DIALECT, null);
if (dialectClassName == null) {
try {
dialect = new DialectResolver().getDialect(dataSource);
} catch (JdbcStoreException e) {
throw new ConfigurationException("Failed to auto detect dialect", e);
}
} else {
try {
dialect = (Dialect) ClassUtils.forName(dialectClassName, settings.getClassLoader()).newInstance();
} catch (Exception e) {
throw new ConfigurationException("Failed to configure dialect [" + dialectClassName + "]");
}
}
if (log.isDebugEnabled()) {
log.debug("Using dialect [" + dialect.getClass().getName() + "]");
}
managed = settings.getSettingAsBoolean(LuceneEnvironment.JdbcStore.MANAGED, false);
if (log.isDebugEnabled()) {
log.debug("Using managed [" + managed + "]");
}
if (!managed) {
this.dataSource = new TransactionAwareDataSourceProxy(this.dataSource);
}
disableSchemaOperation = settings.getSettingAsBoolean(LuceneEnvironment.JdbcStore.DISABLE_SCHEMA_OPERATIONS, false);
if (log.isDebugEnabled()) {
log.debug("Using disable schema operations [" + disableSchemaOperation + "]");
}
jdbcSettings = new JdbcDirectorySettings();
jdbcSettings.setNameColumnName(settings.getSetting(LuceneEnvironment.JdbcStore.DDL.NAME_NAME, jdbcSettings.getNameColumnName()));
jdbcSettings.setValueColumnName(settings.getSetting(LuceneEnvironment.JdbcStore.DDL.VALUE_NAME, jdbcSettings.getValueColumnName()));
jdbcSettings.setSizeColumnName(settings.getSetting(LuceneEnvironment.JdbcStore.DDL.SIZE_NAME, jdbcSettings.getSizeColumnName()));
jdbcSettings.setLastModifiedColumnName(settings.getSetting(LuceneEnvironment.JdbcStore.DDL.LAST_MODIFIED_NAME, jdbcSettings.getLastModifiedColumnName()));
jdbcSettings.setDeletedColumnName(settings.getSetting(LuceneEnvironment.JdbcStore.DDL.DELETED_NAME, jdbcSettings.getDeletedColumnName()));
jdbcSettings.setNameColumnLength(settings.getSettingAsInt(LuceneEnvironment.JdbcStore.DDL.NAME_LENGTH, jdbcSettings.getNameColumnLength()));
jdbcSettings.setValueColumnLengthInK(settings.getSettingAsInt(LuceneEnvironment.JdbcStore.DDL.VALUE_LENGTH, jdbcSettings.getValueColumnLengthInK()));
jdbcSettings.setDeleteMarkDeletedDelta(settings.getSettingAsLong(LuceneEnvironment.JdbcStore.DELETE_MARK_DELETED_DELTA, jdbcSettings.getDeleteMarkDeletedDelta()));
if (log.isDebugEnabled()) {
log.debug("Using delete mark deleted older than [" + jdbcSettings.getDeleteMarkDeletedDelta() + "ms]");
}
jdbcSettings.setQueryTimeout(settings.getSettingAsInt(LuceneEnvironment.Transaction.LOCK_TIMEOUT, jdbcSettings.getQueryTimeout()));
if (log.isDebugEnabled()) {
log.debug("Using query timeout (transaction lock timeout) [" + jdbcSettings.getQueryTimeout() + "ms]");
}
try {
jdbcSettings.setLockClass(settings.getSettingAsClass(LuceneEnvironment.JdbcStore.LOCK_TYPE, jdbcSettings.getLockClass()));
} catch (ClassNotFoundException e) {
throw new CompassException("Failed to create jdbc lock class [" + settings.getSetting(LuceneEnvironment.JdbcStore.LOCK_TYPE) + "]");
}
if (log.isDebugEnabled()) {
log.debug("Using lock strategy [" + jdbcSettings.getLockClass().getName() + "]");
}
if (dialect.supportTransactionalScopedBlobs() &&
!"true".equalsIgnoreCase(settings.getSetting(LuceneEnvironment.JdbcStore.Connection.AUTO_COMMIT, "false"))) {
// Use FetchPerTransaction is dialect supports it
jdbcSettings.getDefaultFileEntrySettings().setClassSetting(JdbcFileEntrySettings.INDEX_INPUT_TYPE_SETTING,
FetchPerTransactionJdbcIndexInput.class);
if (log.isDebugEnabled()) {
log.debug("Using transactional blobs (dialect supports it)");
}
} else {
if (log.isDebugEnabled()) {
log.debug("Using non transactional blobs (dialect does not supports it)");
}
}
Map fileEntries = settings.getSettingGroups(LuceneEnvironment.JdbcStore.FileEntry.PREFIX);
for (Iterator it = fileEntries.keySet().iterator(); it.hasNext();) {
String fileEntryName = (String) it.next();
CompassSettings compassFeSettings = (CompassSettings) fileEntries.get(fileEntryName);
if (log.isInfoEnabled()) {
log.info("Configuring file entry [" + fileEntryName + "] with settings [" + compassFeSettings + "]");
}
JdbcFileEntrySettings jdbcFileEntrySettings = jdbcSettings.getFileEntrySettingsWithoutDefault(fileEntryName);
if (jdbcFileEntrySettings == null) {
jdbcFileEntrySettings = new JdbcFileEntrySettings();
}
// iterate over all the settings and copy them to the jdbc settings
for (Iterator feIt = compassFeSettings.keySet().iterator(); feIt.hasNext();) {
String feSetting = (String) feIt.next();
jdbcFileEntrySettings.setSetting(feSetting, compassFeSettings.getSetting(feSetting));
}
jdbcSettings.registerFileEntrySettings(fileEntryName, jdbcFileEntrySettings);
}
}
public Directory open(String subContext, String subIndex) throws SearchEngineException {
String totalPath = subContext + "_" + subIndex;
JdbcTable jdbcTable = cachedJdbcTables.get(totalPath);
if (jdbcTable == null) {
jdbcTable = new JdbcTable(jdbcSettings, dialect, totalPath);
cachedJdbcTables.put(totalPath, jdbcTable);
}
JdbcDirectory dir = new JdbcDirectory(dataSource, jdbcTable);
if (!disableSchemaOperation) {
try {
Boolean exists = indexExists(dir);
if (exists == null) {
try {
exists = IndexReader.indexExists(dir);
} catch (IOException e) {
exists = false;
}
}
if (!exists) {
dir.create();
}
} catch (IOException e) {
throw new SearchEngineException("Failed to create dir [" + totalPath + "]", e);
}
}
return dir;
}
public Boolean indexExists(Directory dir) throws SearchEngineException {
try {
// for databases that fail if there is no table (like postgres)
if (dialect.supportsTableExists()) {
boolean tableExists = ((JdbcDirectory) dir).tableExists();
if (!tableExists) {
return Boolean.FALSE;
}
}
} catch (IOException e) {
log.warn("Failed to check if index exists", e);
} catch (UnsupportedOperationException e) {
// do nothing, let the base class check for it
}
return null;
}
public void deleteIndex(Directory dir, String subContext, String subIndex) throws SearchEngineException {
try {
if (disableSchemaOperation) {
((JdbcDirectory) dir).deleteContent();
} else {
((JdbcDirectory) dir).delete();
}
} catch (IOException e) {
throw new SearchEngineException("Failed to delete index [" + subIndex + "]", e);
}
}
public void cleanIndex(Directory dir, String subContext, String subIndex) throws SearchEngineException {
JdbcDirectory jdbcDirectory = (JdbcDirectory) dir;
try {
jdbcDirectory.deleteContent();
} catch (IOException e) {
throw new SearchEngineException("Failed to delete content of [" + subIndex + "]", e);
}
}
public void performScheduledTasks(Directory dir, String subContext, String subIndex) throws SearchEngineException {
try {
((JdbcDirectory) dir).deleteMarkDeleted();
} catch (IOException e) {
throw new SearchEngineException("Failed to delete mark deleted with jdbc for [" + subIndex + "]", e);
}
}
public CopyFromHolder beforeCopyFrom(String subContext, String subIndex, Directory dir) throws SearchEngineException {
try {
((JdbcDirectory) dir).deleteContent();
} catch (IOException e) {
throw new SearchEngineException("Failed to delete index content");
}
return new CopyFromHolder();
}
public void registerEventListeners(SearchEngine searchEngine, SearchEngineEventManager eventManager) {
if (managed) {
eventManager.registerLifecycleListener(new ManagedEventListeners());
} else {
eventManager.registerLifecycleListener(new NoneManagedEventListeners());
}
}
public void close() {
this.dataSourceProvider.closeDataSource();
}
/**
* The Jdbc store does require transactional context when executing async operations.
*/
public boolean requiresAsyncTransactionalContext() {
return true;
}
private class ManagedEventListeners implements SearchEngineLifecycleEventListener {
public void beforeBeginTransaction() throws SearchEngineException {
}
public void afterBeginTransaction() throws SearchEngineException {
}
public void afterPrepare() throws SearchEngineException {
}
public void afterCommit(boolean onePhase) throws SearchEngineException {
Connection conn;
try {
conn = DataSourceUtils.getConnection(dataSource);
} catch (JdbcStoreException e) {
throw new SearchEngineException("Failed to get connection", e);
}
FetchPerTransactionJdbcIndexInput.releaseBlobs(conn);
DataSourceUtils.releaseConnection(conn);
}
public void afterRollback() throws SearchEngineException {
Connection conn;
try {
conn = DataSourceUtils.getConnection(dataSource);
} catch (JdbcStoreException e) {
throw new SearchEngineException("Failed to get connection", e);
}
FetchPerTransactionJdbcIndexInput.releaseBlobs(conn);
DataSourceUtils.releaseConnection(conn);
}
public void close() throws SearchEngineException {
}
}
private class NoneManagedEventListeners implements SearchEngineLifecycleEventListener {
private Connection connection;
public void beforeBeginTransaction() throws SearchEngineException {
try {
connection = DataSourceUtils.getConnection(dataSource);
} catch (JdbcStoreException e) {
throw new SearchEngineException("Failed to open db connection", e);
}
}
public void afterBeginTransaction() throws SearchEngineException {
}
public void afterPrepare() throws SearchEngineException {
}
public void afterCommit(boolean onePhase) throws SearchEngineException {
try {
DataSourceUtils.commitConnectionIfPossible(connection);
} catch (JdbcStoreException e) {
throw new SearchEngineException("Failed to commit database transcation", e);
} finally {
DataSourceUtils.releaseConnection(connection);
this.connection = null;
}
}
public void afterRollback() throws SearchEngineException {
try {
DataSourceUtils.rollbackConnectionIfPossible(connection);
} catch (JdbcStoreException e) {
throw new SearchEngineException("Failed to rollback database transcation", e);
} finally {
DataSourceUtils.releaseConnection(connection);
this.connection = null;
}
}
public void close() throws SearchEngineException {
}
}
}