// You can redistribute this software and/or modify it under the terms of
// the Ozone Library License version 1 published by ozone-db.org.
//
// The original code and portions created by SMB are
// Copyright (C) 1997-@year@ by SMB GmbH. All rights reserved.
//
// $Id: ExternalDatabase.java,v 1.5 2002/06/25 11:34:38 mediumnet Exp $
package org.ozoneDB;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Hashtable;
import java.util.StringTokenizer;
import javax.transaction.xa.XAResource;
import org.ozoneDB.DxLib.*;
import org.ozoneDB.core.DbRemote.*;
import org.ozoneDB.core.Transaction;
import org.ozoneDB.core.TransactionID;
import org.ozoneDB.core.admin.Admin;
import org.ozoneDB.core.admin.AdminImpl;
import org.ozoneDB.xa.OzoneXAResource;
import org.ozoneDB.xml.util.SAXChunkConsumer;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
/**
* Base class for implementations of the OzoneInterface which are used from
* a client application to access an ozone.<p>
*
* Each thread is associated with a server connection. This connection is used
* if the thread is not associated with a transaction. Otherwise the connection
* of the transaction is used. So in case the thread has *joined* a transaction
* it does not use its own connection but the connection of the transaction.
* The prepare/commit/abort methods don't need to be called from the thread that
* is joined to the transaction.<p>
*
* Impl. note: The OzoneInterface methods don't need to be synchronized any
* longer because each threads has its own connection.<p>
*
* @author <a href="http://www.softwarebuero.de/">SMB</a>
* @author <a href="http://www.medium.net/">Medium.net</a>
* @version $Revision: 1.5 $Date: 2002/06/25 11:34:38 $
* @see OzoneInterface
*/
public abstract class ExternalDatabase extends AbstractDatabase implements OzoneInterface {
// Constants
public final static String PROP_HOST = "host";
public final static String PROP_PORT = "port";
public final static String PROP_DIR = "dir";
public final static String PROP_USER = "user";
public final static String PROP_PASSWD = "passwd";
public final static String PROP_DEBUG = "debug";
// Data
/**
* The wrapper that uses this as its delegate or null if there is no such
* wrapper. Especially the linkForProxy() calls are redirected to the
* wrapper to have the proxies link to the wrapper instead of this database.
*/
private ExternalDatabase wrapper;
/**
* Table of all current external transactions. thread -> transaction
*/
private DxMap txTable;
/**
* Pool of currently available database connections.
*/
private DxDeque apool;
/**
* Pool of currently used database connections.
*/
private DxSet upool;
/**
* The XAResource of this database connection.
*/
private XAResource xares;
/**
* If this is non-null it is used by all threads as the current transaction.
*/
// private AbstractTransaction globalTX;
/**
* All ExternalDatabase objects that have been created in the current VM.
* This is used by the forThread() method.
*/
private static DxBag databases = new DxArrayBag();
/**
* Static method to find a database connection that has an associated
* transaction that has been joined by the given thread. Used by proxy
* constructors to determine its database. This method returns the first
* database that was found.
*
*
* @param _thread The thread for which to find the corresponding database.
* @return The database for the given thread.
*/
public static ExternalDatabase forThread(Thread _thread) {
DxIterator it = databases.iterator();
ExternalDatabase db;
while ((db = (ExternalDatabase) it.next()) != null) {
DxSet threads = db.txTable.keySet();
if (threads.contains(_thread)) {
return db;
}
}
return null;
}
// Constructors ***************************************
public ExternalDatabase() {
}
// XAResource factory *********************************
/**
* Return a new XAResource for this database connection.
* @return new XAResource.
*/
public final XAResource getXAResource() {
if (xares == null) {
synchronized (this) {
if (xares == null) {
xares = new OzoneXAResource(this);
}
}
}
return xares;
}
// external transaction handling **********************
protected final AbstractTransaction txForThread(Thread thread) {
return (AbstractTransaction) txTable.elementForKey(thread);
}
/**
* Create a new transaction. Before using this transaction it must be started.
*
* @return The newly created transaction.
*/
public ExternalTransaction newTransaction() {
return new ExternalTransaction(this);
}
/**
* Obtain the {@link ExternalTransaction} that is assocaited to the
* caller's thread or null, if there is no such transaction. This does not
* necessarily mean that there is no transaction associated to this
* thread at all. For example there might be an XA transaction.
*
* @return The transaction that is associated to the caller's thread.
* @see #currentTransaction()
*/
public ExternalTransaction currentExternalTransaction() {
AbstractTransaction atx = txForThread(Thread.currentThread());
if (atx instanceof ExternalTransaction) {
return (ExternalTransaction) atx;
} else {
return null;
}
}
/**
* Obtain the transaction that is assocaited to the caller's thread or null,
* if there is no such transaction. The transaction might be an instance
* of any subclass of AbstractTransaction, which includes also non-user
* transactions like {@link org.ozoneDB.xa.XATransaction}.
*
* @return The transaction that is associated to the caller's thread.
* @see #currentExternalTransaction()
*/
public AbstractTransaction currentTransaction() {
return txForThread(Thread.currentThread());
}
/**
* This method is never directly called from the client code.
*/
public void beginTX(AbstractTransaction tx) throws TransactionExc, IOException {
// check if the transaction is already started
if (tx.connection != null) {
throw new TransactionExc("Transaction already started.", TransactionExc.STATE);
}
synchronized (tx) {
try {
tx.connection = acquirePooledConnection();
joinTX(tx);
// exceptions are catched and re-thrown be sendCommand()
TransactionID taID = (TransactionID) sendCommand(new DbTransaction(DbTransaction.MODE_BEGIN), true, tx.connection);
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new TransactionExc(e.toString(), TransactionExc.UNEXPECTED);
}
}
}
/**
* This method is never directly called from the client code.
*/
public void joinTX(AbstractTransaction tx) throws TransactionExc {
Thread thread = Thread.currentThread();
synchronized (txTable) {
// check if the current thread is already joined to a transaction
AbstractTransaction txOfThread = (AbstractTransaction) txTable.elementForKey(thread);
if (txOfThread != null && txOfThread != tx) {
throw new TransactionExc("Thread is already joined to a transaction.", TransactionExc.STATE);
}
txTable.addForKey(tx, thread);
}
}
/**
* This method is never directly called from the client code.
*/
public boolean leaveTX(AbstractTransaction tx) {
return txTable.removeForKey(Thread.currentThread()) != null;
}
/**
* This method is never directly called from the client code.
*/
public void checkpointTX(AbstractTransaction tx) throws TransactionExc, IOException {
commandTX(tx, new DbTransaction(DbTransaction.MODE_CHECKPOINT));
}
/**
* This method is never directly called from the client code.
*/
public void prepareTX(AbstractTransaction tx) throws TransactionExc, IOException {
if (tx.connection == null) {
throw new TransactionExc("Illegal state.", TransactionExc.STATE);
}
commandTX(tx, new DbTransaction(DbTransaction.MODE_PREPARE));
}
/**
* This method is never directly called from the client code.
*/
public void commitTX(AbstractTransaction tx, boolean onePhase) throws TransactionExc, IOException {
synchronized (tx) {
if (tx.connection == null) {
throw new TransactionExc("Illegal state.", TransactionExc.STATE);
}
if (onePhase) {
commandTX(tx, new DbTransaction(DbTransaction.MODE_COMMIT_ONEPHASE));
} else {
commandTX(tx, new DbTransaction(DbTransaction.MODE_COMMIT_TWOPHASE));
}
releasePooledConnection(tx.connection);
tx.connection = null;
}
synchronized (txTable) {
// disassociate all threads that have joined the transaction
DxIterator it = txTable.iterator();
AbstractTransaction cursorTX;
while ((cursorTX = (AbstractTransaction) it.next()) != null) {
if (cursorTX == tx) {
it.removeObject();
}
}
}
}
/**
* This method is never directly called from the client code.
*/
public void rollbackTX(AbstractTransaction tx) throws TransactionExc, IOException {
synchronized (tx) {
if (tx.connection == null) {
// don't throw an exception to allow subsequent calls
return;
}
commandTX(tx, new DbTransaction(DbTransaction.MODE_ABORT));
releasePooledConnection(tx.connection);
tx.connection = null;
}
synchronized (txTable) {
// disassociate all threads that have joined the transaction
DxIterator it = txTable.iterator();
AbstractTransaction cursorTX;
while ((cursorTX = (AbstractTransaction) it.next()) != null) {
if (cursorTX == tx) {
it.removeObject();
}
}
}
}
/**
* Obtain the _internal_ server status of this transaction.
* @return Status of the transaction. Defined in
* {@link org.ozoneDB.core.Transaction}.<p>
*
* This method is never directly called from the client code.
*/
public final int getStatusTX(AbstractTransaction tx) throws TransactionExc, IOException {
if (tx.connection == null) {
return Transaction.STATUS_NONE;
} else {
DbTransaction command = new DbTransaction(DbTransaction.MODE_STATUS);
commandTX(tx, command);
return ((Integer) command.result).intValue();
}
}
/**
* This method is never directly called from the client code.
*/
protected final Object commandTX(AbstractTransaction tx, DbTransaction command) throws TransactionExc, IOException {
if (tx.connection == null) {
throw new TransactionExc("Thread has not yet joined a transaction.", TransactionExc.STATE);
}
try {
// exceptions are catched and re-thrown be sendCommand();
// use the connection of the transaction to allow non-joined
// transaction to complete the transaction
return sendCommand(command, true, tx.connection);
} catch (IOException e) {
throw e;
} catch (TransactionExc e) {
throw e;
} catch (Exception e) {
throw new TransactionExc(e.toString(), TransactionExc.UNEXPECTED);
}
}
// connection pooling *********************************
/**
* The way for the actual database types to create a corresponding
* connection.
*/
protected abstract DbClient newConnection() throws Exception;
/**
* Get a connection from the connection pool.<p>
*
* Earlier versions kept one connection for each client thread. That is,
* client threads were directly mapped to server threads. Especially for
* servlet environment this produces a lot of connections (server threads),
* which in fact is not needed.<p>
*
* So we are using a pool of connections now. Connections are actually
* created and added to the pool when the pool is empty. Currently connections
* are never closed once they are added to the pool.
*
* @param DbClient A connection from the pool.
*/
protected final DbClient acquirePooledConnection() throws Exception {
// keep both pools stable while we are messing with them
synchronized (apool) {
synchronized (upool) {
if (apool.isEmpty()) {
DbClient connection = newConnection();
apool.push(connection);
}
DbClient connection = (DbClient) apool.pop();
if (upool.add(connection) == false) {
throw new IllegalStateException("Connection is already in use.");
}
return connection;
}
}
}
/**
* Release a formerly acquired pooled connection so that it may used by
* another request.
*
* @param connection The pooled connection to be released
* @see #acquirePooledConnection
*/
protected final void releasePooledConnection(DbClient connection) {
// keep both pools stable while we are messing with it
synchronized (apool) {
synchronized (upool) {
if (upool.remove(connection) == false) {
throw new IllegalStateException("Given connection is not element of the pool of used connections.");
}
apool.push(connection);
}
}
}
/**
* Return the server connection for the specified thread. If the thread is
* not yet joined to a connection create a new one and associate with the
* thread.
*/
// protected final DbClient connectionForThread( Thread thread ) throws Exception {
// if (!isOpen()) {
// throw new RuntimeException( "Database not open" );
// }
//
// // is there a connection associated to this thread?
// DbClient connection = (DbClient)connectionTable.elementForKey( thread );
//
// if (connection == null) {
// synchronized (connectionTable) {
// // close all connections that are no longer used by a thread;
// // don't re-use connections because they are not really stateless
// DxIterator it = connectionTable.iterator();
// while (it.next() != null) {
// if (!((Thread)it.key()).isAlive()) {
// // System.out.println ("closing connection...");
// connection = (DbClient)it.removeObject();
// connection.send( new DbCloseConn() );
// connection.close();
// }
// }
// // make a new connection
// connection = newConnection();
// connectionTable.addForKey( connection, thread );
// }
// }
// return connection;
// }
/**
* Send the specified command to the server. If there is a global
* transaction, its connection is used. Else, if the current thread is joined
* to a transaction the connection of the transaction is used. Otherwise a
* connection from the pool is used.
*/
protected final Object sendCommand(DbCommand command, boolean waitForResult) throws Exception {
if (!isOpen()) {
throw new IllegalStateException("Database is not open.");
}
Thread thread = Thread.currentThread();
AbstractTransaction txOfThread = (AbstractTransaction) txTable.elementForKey(thread);
if (txOfThread != null) {
DbClient connection = txOfThread.connection;
return sendCommand(command, waitForResult, connection);
} else {
DbClient connection = null;
try {
connection = acquirePooledConnection();
return sendCommand(command, waitForResult, connection);
} finally {
releasePooledConnection(connection);
}
}
}
/**
* Send the specified command to the server. Use the specified connection.
* While working the connection is synchronized to allow multiple threads
* to use this connection.
*
* @param waitForResult
* true: read the result from the external database and return it
* false: do not read the result from the external database and return null.
* This is dangerous if not properly used. In this case, the result
* is not read from the stream and left to be read from the next reader.
* As this is not desireable, only supply false if the command does not return any result.
*/
protected Object sendCommand( DbCommand command, boolean waitForResult, DbClient connection ) throws Exception,ExceptionInOzoneObjectException {
Object result = null;
// this allows multiple thread to use one connection; this happens,
// if a thread joines a thransaction instead of beginning it
synchronized (connection) {
connection.send(command);
if (waitForResult) {
//read the result and set proxy links
result = connection.receive();
if (result != null) {
// FIXME: It is not wise to overload the result with both a normal result and an exception.
if (result instanceof RuntimeException) {
if (result instanceof ExceptionInOzoneObjectException) {
throw (ExceptionInOzoneObjectException) result;
} else {
((Exception)result).fillInStackTrace();
throw (RuntimeException)result;
}
} else if (result instanceof Exception) {
((Exception)result).fillInStackTrace();
throw (Exception)result;
} else {
if (result instanceof Error) {
((Error) result).fillInStackTrace();
throw (Error) result;
}
}
}
}
}
return result;
}
protected ExternalDatabase linkForProxy(OzoneProxy proxy) {
ExternalDatabase link = wrapper != null ? wrapper : this;
// System.out.println ("*** linkForProxy(): " + link.getClass().getName());
return link;
}
protected synchronized void setWrapper(ExternalDatabase _wrapper) {
wrapper = _wrapper;
}
// OzoneInterface methods *****************************
public boolean isOpen() throws Exception {
return txTable != null;
}
/**
* Open this database connection according to the specified properties.
*/
protected void open(Hashtable _props) throws Exception {
txTable = new DxHashMap();
apool = new DxArrayDeque(32);
upool = new DxHashSet(32);
databases.add(this);
}
/**
* Factory method that creates a new database object. The actual type of the
* database object ({@link ExternalDatabase} or {@link LocalDatabase})
* depends on the specified database URL. The returned database connection
* is already open.
*/
public static ExternalDatabase openDatabase(String _url, String _username, String _passwd) throws Exception {
Hashtable props = createProps(_url);
props.put(PROP_USER, _username);
props.put(PROP_PASSWD, _passwd);
if (_url.startsWith("ozonedb:remote")) {
RemoteDatabase db = new RemoteDatabase();
db.open(props);
return db;
} else if (_url.startsWith("ozonedb:local")) {
LocalDatabase db = new LocalDatabase();
db.open(props);
return db;
} else {
throw new MalformedURLException(_url);
}
}
/**
* Factory method that creates a new database object. The actual type of the
* database object ({@link ExternalDatabase} or {@link LocalDatabase})
* depends on the specified database URL. The returned database connection
* is already open.
*/
public static ExternalDatabase openDatabase(String _url) throws Exception {
Hashtable props = createProps(_url);
String userName = System.getProperty("org.ozoneDB.user.name");
if (userName == null) {
userName = System.getProperty("user.name");
}
props.put(PROP_USER, userName);
props.put(PROP_PASSWD, userName);
if (_url.startsWith("ozonedb:remote")) {
RemoteDatabase db = new RemoteDatabase();
db.open(props);
return db;
} else if (_url.startsWith("ozonedb:local")) {
LocalDatabase db = new LocalDatabase();
db.open(props);
return db;
} else {
throw new MalformedURLException(_url);
}
}
/**
* @param _url The URL of the database (ozonedb:remote://host:port or
* ozonedb:local://datadir)
*/
protected static Hashtable createProps(String _url) throws MalformedURLException {
Hashtable props = new Hashtable();
StringTokenizer tkn = new StringTokenizer(_url, ":");
String protocol = tkn.nextToken();
if (!protocol.equals("ozonedb")) {
throw new MalformedURLException("protocol: " + protocol);
}
String protocol2 = tkn.nextToken();
if (protocol2.equals("local")) {
String filename = tkn.nextToken();
// on win the filename may contain ":"
if (tkn.hasMoreTokens()) {
filename = filename + ":" + tkn.nextToken();
}
props.put(PROP_DIR, filename);
} else if (protocol2.equals("remote")) {
String hostname = tkn.nextToken();
if (!hostname.startsWith("//")) {
throw new MalformedURLException("hostname: " + hostname);
}
// skip the leading "//"
hostname = hostname.substring(2);
int port = Integer.parseInt(tkn.nextToken());
props.put(PROP_HOST, hostname);
props.put(PROP_PORT, new Integer(port));
} else {
throw new MalformedURLException("protocol: " + protocol2);
}
return props;
}
/**
* Close this database.
*/
public synchronized void close() throws Exception {
if (isOpen()) {
databases.remove(this);
// close all current database connections.
try {
apool.addAll(upool);
DxIterator it = apool.iterator();
DbClient connection;
while ((connection = (DbClient) it.next()) != null) {
connection.send(new DbCloseConn());
Thread.sleep(1000);
connection.close();
it.removeObject();
}
} finally {
apool = null;
upool = null;
txTable = null;
}
}
}
protected void finalize() throws Throwable {
close();
}
public void reloadClasses() throws Exception {
Integer result = (Integer) sendCommand(new DbReloadClasses(), true);
}
public OzoneProxy createObject( String className, int access, String name, String sig, Object[] args ) throws RuntimeException,ExceptionInOzoneObjectException {
try {
OzoneProxy proxy = (OzoneProxy) sendCommand( new DbCreateObj( className, access, name, sig, args ), true );
if (org.ozoneDB.core.Env.selfCheck) {
if (proxy==null) { // The proxy should never be null.
throw new Error("Found during createObject(\""+className+"\","+access+","+name+",\""+sig+"\"): returned proxy is "+proxy);
}
}
return proxy;
} catch (ExceptionInOzoneObjectException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
// only supported from JDK1.4 on
// throw new RuntimeException("Caught during createObject(\""+className+"\","+access+","+name+",\""+sig+"\")",e);
throw new RuntimeException("Caught during createObject(\""+className+"\","+access+","+name+",\""+sig+"\"): "+e);
}
}
public void deleteObject( OzoneRemote obj ) throws RuntimeException,ExceptionInOzoneObjectException {
try {
sendCommand( new DbDeleteObj( (OzoneProxy)obj ), true );
} catch (ExceptionInOzoneObjectException e) {
throw e;
} catch (Exception e) {
// only supported from JDK1.4 on
// throw new RuntimeException("Caught during deleteObject()",e);
throw new RuntimeException("Caught during deleteObject(): "+e);
}
}
public OzoneProxy copyObject(OzoneRemote obj) throws Exception {
return (OzoneProxy) sendCommand(new DbCopyObj((OzoneProxy) obj), true);
}
public void nameObject(OzoneRemote obj, String name) throws Exception {
sendCommand(new DbNameObj((OzoneProxy) obj, name), true);
}
public OzoneProxy objectForName(String name) throws Exception {
return (OzoneProxy) sendCommand(new DbObjForName(name), true);
}
public OzoneProxy objectForHandle(String handle) throws Exception {
return (OzoneProxy) sendCommand(new DbObjForHandle(handle), true);
}
public OzoneProxy[] objectsOfClass(String name) throws Exception {
throw new RuntimeException("Method not implemented.");
}
public Object invoke(OzoneProxy rObj, String methodName, String sig, Object[] args, int lockLevel)
throws Exception {
// Kommando verschicken
Object result = sendCommand(new DbInvoke(rObj, methodName, sig, args, lockLevel), true);
return result;
}
public Object invoke(OzoneProxy rObj, int methodIndex, Object[] args, int lockLevel) throws Exception {
// Kommando verschicken
Object result = sendCommand(new DbInvoke(rObj, methodIndex, args, lockLevel), true);
return result;
}
public OzoneCompatible fetch(OzoneProxy rObj, int lockLevel) throws Exception {
return null;
}
public Node xmlForObject(OzoneRemote rObj, Document domFactory) throws Exception {
byte[] bytes = (byte[]) sendCommand(new DbXMLForObj((OzoneProxy) rObj), true);
SAXChunkConsumer consumer = new SAXChunkConsumer(domFactory, null);
consumer.processChunk(bytes);
return consumer.getResultNode();
}
public void xmlForObject(OzoneRemote rObj, ContentHandler ch) throws Exception {
byte[] bytes = (byte[]) sendCommand(new DbXMLForObj((OzoneProxy) rObj), true);
SAXChunkConsumer consumer = new SAXChunkConsumer(ch);
consumer.processChunk(bytes);
}
/**
* Return the administration object for this database.
*
* @return The admin object for this database;
*/
public Admin admin() throws Exception {
Admin admin = (Admin) objectForName(AdminImpl.OBJECT_NAME);
return admin;
}
/**
Internal method. This method is called by {@link OzoneProxy}s when they are dying (during finalize()). This
is required, as the database may track the references the database client has to objects within the database
in order to properly support garbage collection. If this method is called from anyone else than from the
{@link OzoneProxy}.finalize()-Method, data loss may occur!
@param proxy the OzoneProxy object which is dying. It may call this method exaclty once.
*/
public void notifyProxyDeath(OzoneProxy proxy) {
try {
sendCommand(new DbProxyDeath(proxy),false);
} catch (Exception e) {
// only supported from JDK1.4 on
// throw new RuntimeException("Caught during notifyProxyDeath()",e);
throw new RuntimeException("Caught during notifyProxyDeath(): "+e);
}
}
}