/*
* 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: Database.java 533273 2007-04-28 00:51:37Z vgritsenko $
*/
package org.apache.xindice.core;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xindice.core.query.QueryEngine;
import org.apache.xindice.server.Xindice;
import org.apache.xindice.util.Configuration;
import org.apache.xindice.util.ConfigurationException;
import org.apache.xindice.util.Named;
import org.apache.xindice.util.XindiceException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
/**
* Database is the primary container for the Xindice Database Engine.
*
* @version $Revision: 533273 $, $Date: 2007-04-27 20:51:37 -0400 (Fri, 27 Apr 2007) $
*/
public final class Database extends Collection
implements Named {
private static final Log log = LogFactory.getLog(Database.class);
// Configuration elements in config/system.xml file
public static final String DBROOT = "dbroot";
public static final String NAME = "name";
private static final String QUERYENGINE = "queryengine";
private static final String DATABASE = "database";
private static final String METADATA = "use-metadata";
public static final String DBROOT_DEFAULT = "./db/";
// Configuration documents stored in SysConfigs collection
private static final String COLKEY = "database.xml";
private static final String METAKEY = "meta.xml";
private static final Map databases = new HashMap(); // String to Database
private static final DatabaseShutdownHandler shutdownHandler = new DatabaseShutdownHandler();
static {
// sets up our golbal observer. will automatically flush document
// changes to disk.
DBObserver.setInstance(new DatabaseChangeObserver());
}
/**
* This will return an instance of a Database for the given
* name if one has already been loaded, otherwise it will
* create a new instance.
*
* @param config Database configuration
* @return Database instance
* @throws ConfigurationException if database name is missing in the configuration
*/
public static Database getDatabase(Configuration config) {
String name = config.getAttribute(Database.NAME);
// No name in the config file ... can't get the database
if (name == null) {
throw new ConfigurationException("Database configuration didn't contain a database name");
}
Database database = (Database) databases.get(name);
if (database == null) {
// In case it's currently being added (only pay the sync hit on a miss)
synchronized (databases) {
// Was it created while we waited?
database = (Database) databases.get(name);
if (database == null) {
database = new Database();
try {
database.setConfig(config);
} catch (XindiceException x) {
// TODO: Configurable interface should use ConfigurationException instead of XindiceException... Right?
throw new ConfigurationException("XindiceException: " + x.getMessage(), x);
}
databases.put(database.getName(), database);
}
}
}
return database;
}
/**
* This will merely return an instance of a Database for the given
* name if one has already been loaded.
*
* @param name Database name
* @return Database
*/
public static Database getDatabase(String name) {
Database database = (Database) databases.get(name);
if (database == null) {
// in case it's currently being added (only pay the sync hit on a miss)
synchronized (databases) {
database = (Database) databases.get(name);
}
}
return database;
}
public static String[] listDatabases() {
synchronized (databases) {
return (String[]) databases.keySet().toArray(new String[0]);
}
}
//
// Instance...
//
private DocumentCache docCache;
private QueryEngine engine;
private boolean metaEnabled;
private boolean metaInit;
private MetaSystemCollection metaSystemCollection;
private boolean sysInit;
private SystemCollection systemCollection;
private FileOutputStream lock;
private boolean closed;
/** Shared timer instance for this database's IndexManagers */
private Timer timer;
public Database() {
super();
docCache = new DocumentCache();
engine = new QueryEngine(this);
closed = true;
}
/**
* Checks to see if it has been closed to insure that it doesn't try to do it
* twice.
*
* @param removeFromShutdown If true removes its self from the shutdown hook
* @return true if closed
* @throws DBException if unable to close
*/
protected synchronized boolean close(boolean removeFromShutdown) throws DBException {
if (removeFromShutdown) {
// we have already been closed so no need to do this again.
shutdownHandler.removeDatabase(this);
}
// check to see if we have already been closed.
if (!closed) {
if(log.isDebugEnabled()) {
log.debug("Shutting down database: '" + getName() + "'");
}
flushConfig();
super.close();
// Release database lock
try {
this.lock.close();
new File(getCollectionRoot(), "db.lock").delete();
} catch (Exception e) {
// Ignore IO exception
}
this.lock = null;
synchronized (databases) {
databases.remove(getName());
}
// Stop the timer thread
timer.cancel();
closed = true;
}
return true;
}
/**
* @see org.apache.xindice.core.DBObject#close()
* @see org.apache.xindice.core.Database#close(boolean removeFromShutdown)
*/
public boolean close() throws DBException {
return close(true);
}
/**
* flushConfig ensures that the Collection configuration has been
* properly flushed to disk after a modification.
*/
public void flushConfig() {
if (getConfig().isDirty()) {
try {
Document d = getConfig().getElement().getOwnerDocument();
systemCollection.getCollection(SystemCollection.CONFIGS).setDocument(COLKEY, d);
getConfig().resetDirty();
} catch (Exception e) {
log.error("Error Writing Configuration '" + COLKEY + "', for database " + getName(), e);
}
// Observer
DBObserver.getInstance().flushDatabaseConfig(this, getConfig());
}
if (isMetaEnabled()) {
Configuration config = metaSystemCollection.getConfig();
if (config.isDirty()) {
try {
Document d = config.getElement().getOwnerDocument();
systemCollection.getCollection(SystemCollection.CONFIGS).setDocument(METAKEY, d);
config.resetDirty();
} catch (Exception e) {
log.error("Error writing configuration '" + METAKEY + "', for database " + getName(), e);
}
}
}
}
/**
* @see org.apache.xindice.core.Collection#getDatabase()
*/
public Database getDatabase() {
return this;
}
/**
* getDocumentCache returns the Database-level Document Cache.
*
* @return The DocumentCache
*/
public DocumentCache getDocumentCache() {
return docCache;
}
/**
* Return the MetaSystem collection for this database.
*
* It will return null if metadata is not enabled on this database.
* @return MetaSystemCollection
*/
public MetaSystemCollection getMetaSystemCollection() {
return metaSystemCollection;
}
/**
* getQueryEngine returns a reference to the Database's current
* operating QueryEngine implementation.
*
* @return The QueryEngine instance
*/
public QueryEngine getQueryEngine() {
return engine;
}
/**
* @see org.apache.xindice.core.Collection#getSystemCollection()
*/
public SystemCollection getSystemCollection() {
return systemCollection;
}
/**
* Return database's timer instance.
* @return Database timer instance
*/
protected Timer getTimer() {
return timer;
}
/**
* Return whether or not metadata is enabled on this database.
* @return boolean
*/
public boolean isMetaEnabled() {
return metaEnabled;
}
/**
* @see org.apache.xindice.util.Configurable#setConfig(org.apache.xindice.util.Configuration)
*/
public void setConfig(Configuration config) throws XindiceException {
// FIXME Get rid of super.setConfig here?
super.setConfig(config);
setCanonicalName('/' + getName());
// Determine database root directory
String dbroot = config.getAttribute(DBROOT);
File dbrootDir = new File(dbroot);
if (!dbrootDir.isAbsolute()) {
// Here, DB root already should be absolute. XMLRPC, Embed, and Managed drivers take care of it.
log.warn("The specified database root directory '" + dbroot + "' is relative. " +
"Using property " + Xindice.PROP_XINDICE_DB_HOME + " to resolve.");
String home = System.getProperty(Xindice.PROP_XINDICE_DB_HOME);
if (home == null) {
log.warn("The specified database root directory '" + dbroot + "' is relative " +
"and there was no " + Xindice.PROP_XINDICE_DB_HOME + " property set, " +
"so Xindice was unable to determine a database location. " +
"Database will be created relative to the current working directory.");
home = ".";
}
try {
// In case home has been specified as relative path convert it to absolute path
dbrootDir = new File(home, dbroot).getCanonicalFile();
} catch (IOException e) {
throw new XindiceException("Can't get canonical path", e);
}
}
setCollectionRoot(dbrootDir);
if (log.isInfoEnabled()) {
log.info("Database points to " + dbrootDir.getAbsolutePath());
}
// Put a lock (at least attempt to) on the database
// FIXME: Use JDK1.4 FileLock
File lock = new File(getCollectionRoot(), "db.lock");
try {
if (lock.exists() && !lock.delete()) {
throw new IOException("Could not delete lock file.");
}
this.lock = new FileOutputStream(lock);
this.lock.write(new Date().toString().getBytes());
} catch (IOException e) {
throw new XindiceException("Unable to open lock file " + lock + ". " +
"Make sure database is not open by another process. " +
"Exception: " + e);
}
// Now we are ready to open it up
shutdownHandler.registerDatabase(this);
timer = new Timer(false);
closed = false;
// Initialize query engine
try {
Configuration queryCfg = config.getChild(QUERYENGINE);
if (queryCfg != null) {
engine.setConfig(queryCfg);
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
// Initialize system collection
if (!sysInit) {
systemCollection = new SystemCollection(this);
systemCollection.init();
super.addCollection(systemCollection);
this.sysInit = true;
}
Collection sysConfigCollection = systemCollection.getCollection(SystemCollection.CONFIGS);
try {
// Bootstrap from the database itself... This is accomplished
// by intercepting the setConfig call and using a Configuration
// retrieved from the database instead of the standard config
Document colDoc = sysConfigCollection.getDocument(COLKEY);
if (colDoc == null) {
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
colDoc = db.newDocument();
Element root = colDoc.createElement(DATABASE);
root.setAttribute(NAME, getName());
colDoc.appendChild(root);
sysConfigCollection.setDocument(COLKEY, colDoc);
}
super.setConfig(new Configuration(colDoc.getDocumentElement(), false));
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
// Register the Database with the VM
// databases.put(getName(), this);
// initialize the meta collection
// but only if it's turned on in the config.
String metaCfg = config.getAttribute(METADATA);
if (metaCfg.equalsIgnoreCase("on")) {
metaEnabled = true;
}
if (metaEnabled && !metaInit) {
try {
metaSystemCollection = new MetaSystemCollection(this);
Document colDoc = sysConfigCollection.getDocument(METAKEY);
if (colDoc == null) {
metaSystemCollection.init();
Document metaConfig = metaSystemCollection.getConfig().getElement().getOwnerDocument();
sysConfigCollection.setDocument(METAKEY, metaConfig);
} else {
metaSystemCollection.setConfig(new Configuration(colDoc, false));
}
super.addCollection(metaSystemCollection);
metaInit = true;
if (log.isDebugEnabled()) {
log.debug("Meta collection is initialized");
}
} catch (Exception e) {
log.error("Meta collection was not initialized", e);
}
}
// observer
DBObserver.getInstance().setDatabaseConfig(this, getCollections(), config);
}
}