/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* 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 net.ontopia.persistence.proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.WeakHashMap;
import net.ontopia.persistence.query.jdo.JDOQuery;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.PropertyUtils;
import net.ontopia.utils.TraceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* INTERNAL: A storage access implementation accessing relational
* databases using JDBC.
*/
public class RDBMSAccess implements StorageAccessIF {
// Define a logging category.
static Logger log = LoggerFactory.getLogger(RDBMSAccess.class.getName());
protected boolean debug = log.isDebugEnabled();
protected String id;
protected boolean readonly;
protected RDBMSStorage storage;
protected RDBMSMapping mapping;
protected Connection conn_;
protected ThreadLocal<Connection> conn_map = new ThreadLocal<Connection>();
protected boolean closed;
protected Map<Class<?>, ClassAccessIF> handlers;
protected boolean batch_updates = false;
protected Collection<FlushableIF> flushable;
public RDBMSAccess(String id, RDBMSStorage storage, boolean readonly) {
this.id = id;
this.readonly = readonly;
this.storage = storage;
this.mapping = storage.getMapping();
// Enable or disable batch updates
if (PropertyUtils.isTrue(getProperty("net.ontopia.topicmaps.impl.rdbms.BatchUpdates")))
batch_updates = true;
handlers = new HashMap<Class<?>, ClassAccessIF>();
flushable = new HashSet<FlushableIF>();
log.debug(getId() + ": Storage access created");
}
public String getId() {
return id;
}
public StorageIF getStorage() {
return storage;
}
public boolean isReadOnly() {
return readonly;
}
public String getProperty(String property) {
return storage.getProperty(property);
}
// -----------------------------------------------------------------------------
// RDBMS specific
// -----------------------------------------------------------------------------
protected Connection getConn() {
if (readonly)
return conn_map.get();
else
return conn_;
}
protected void setConn(Connection conn) {
if (readonly)
if (conn == null) {
this.conn_map.remove();
} else {
this.conn_map.set(conn);
}
else
this.conn_ = conn;
}
/**
* INTERNAL: Returns the JDBC database connection used. It is important that
* this connection is neither closed, nor commited or rolled back.
*/
public Connection getConnection() {
Connection conn = getConn();
if (conn == null) {
try {
// Request new connection object from storage
conn = storage.getConnectionFactory(readonly).requestConnection();
setConn(conn);
return conn;
} catch (SQLException e) {
throw new OntopiaRuntimeException(e);
}
} else
return conn;
}
public PreparedStatement prepareStatement(String sql) throws SQLException {
return getConnection().prepareStatement(sql);
}
protected synchronized void resetConnection() {
// NOTE: method used to reset connection for read-only access, so
// that a second attempt can be made.
Connection conn = getConn();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// ignore
} finally {
setConn(null);
}
}
}
protected boolean isSQLException(Throwable e) {
if (e == null) return false;
if (e instanceof SQLException)
return true;
else if (e instanceof OntopiaRuntimeException)
return isSQLException(((OntopiaRuntimeException)e).getCause());
else
return false;
}
// -----------------------------------------------------------------------------
// Handlers
// -----------------------------------------------------------------------------
/**
* INTERNAL: Gets up the handler class that is used to manage
* objects of the given class.
*/
protected ClassAccessIF getHandler(Class<?> type) {
// Each class have its own handler
ClassAccessIF handler = handlers.get(type);
// Create class handlers lazily
if (handler == null) {
// TODO: Update class descriptors to no longer depend on java.lang.Class.
ClassInfoIF cinfo = mapping.getClassInfo(type);
switch (cinfo.getStructure()) {
case ClassInfoIF.STRUCTURE_OBJECT: {
handler = (batch_updates ? new SQLBatchObjectAccess(this, cinfo) :
new SQLObjectAccess(this, cinfo));
handlers.put(type, handler);
break;
}
case ClassInfoIF.STRUCTURE_COLLECTION: {
handler = (batch_updates ? new SQLCollectionAccess(this, cinfo) : // FIXME: implement batch
new SQLCollectionAccess(this, cinfo));
handlers.put(type, handler);
break;
}
default:
throw new OntopiaRuntimeException("Unsupported ClassInfoIF structure: " + cinfo.getStructure());
}
}
return handler;
}
// -----------------------------------------------------------------------------
// Connection validation
// -----------------------------------------------------------------------------
public boolean validate() {
Connection conn = getConn();
return !(closed || (conn == null ? false : !validateConnection(conn)));
}
protected boolean validateConnection(Connection conn) {
// stop here if connection says that it's closed
try {
if (conn.isClosed()) return false;
} catch (SQLException e) {
return false;
}
// get validation query
String vquery = getProperty("net.ontopia.topicmaps.impl.rdbms.ConnectionPool.ValidationQuery");
if (vquery == null)
vquery = "select seq_count from TM_ADMIN_SEQUENCE where 1 != 1";
// run validation query
PreparedStatement stm = null;
ResultSet rs = null;
try {
stm = conn.prepareStatement(vquery);
rs = stm.executeQuery();
// do nothing with result set
rs.next();
rs.close();
} catch (SQLException e) {
return false;
} finally {
try {
if (stm != null) stm.close();
} catch (SQLException e) {
return false;
}
}
return true;
}
// -----------------------------------------------------------------------------
// Transactions
// -----------------------------------------------------------------------------
public void commit() {
Connection conn = getConn();
if (conn != null) {
try {
conn.commit();
log.debug(getId() + ": Storage access rw committed.");
} catch (SQLException e) {
throw new OntopiaRuntimeException(e);
}
} else {
log.debug(getId() + ": Storage access committed (no connection).");
}
}
public void abort() {
Connection conn = getConn();
if (conn != null) {
try {
conn.rollback();
log.debug(getId() + ": Storage rw access aborted.");
} catch (SQLException e) {
throw new OntopiaRuntimeException(e);
}
try {
conn.close();
} catch (SQLException e) {
// ignore
} finally {
setConn(null);
}
} else {
log.debug(getId() + ": Storage access aborted (no connection).");
}
}
public void close() {
try {
// Close/release connections
Connection conn = getConn();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// ignore
} finally {
setConn(null);
}
log.debug(getId() + ": Storage access rw closed.");
} else {
log.debug(getId() + ": Storage access closed (no connection).");
}
} finally {
closed = true;
}
}
public void releaseConnection() {
try {
if (!isReadOnly() && (conn_ != null)) {
resetConnection();
} else if (isReadOnly()) {
Connection conn = getConn();
if (conn != null) {
conn.close();
}
}
} catch (SQLException sqle) {
throw new OntopiaRuntimeException(sqle);
}
}
public void flush() {
// Return if nothing to flush
if (flushable.isEmpty()) return;
try {
TraceUtils.enter("RDBMSAccess.flush");
// Flush flushable handlers
for (FlushableIF object : flushable) {
object.flush();
}
flushable.clear();
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
} finally {
TraceUtils.leave("RDBMSAccess.flush");
}
}
public boolean loadObject(AccessRegistrarIF registrar, IdentityIF identity) {
try {
if (debug)
log.debug("Loading object: " + identity);
try {
return getHandler(identity.getType()).load(registrar, identity);
} catch (IdentityNotFoundException e) {
throw e;
} catch (Exception e) {
if (readonly && isSQLException(e)) {
// if read-only, reset connection and try again
resetConnection();
log.warn(getId() + ": Connection seems to be down. Resetting read-only connection before second attempt (loadObject).", e);
return getHandler(identity.getType()).load(registrar, identity);
} else throw e;
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public Object loadField(AccessRegistrarIF registrar, IdentityIF identity, int field) {
try {
if (debug)
log.debug("Loading field: " + field + " identity: " + identity);
try {
return getHandler(identity.getType()).loadField(registrar, identity, field);
} catch (IdentityNotFoundException e) {
throw e;
} catch (Exception e) {
if (readonly && isSQLException(e)) {
// if read-only, reset connection and try again
resetConnection();
log.warn(getId() + ": Connection seems to be down. Resetting read-only connection before second attempt (loadField).", e);
return getHandler(identity.getType()).loadField(registrar, identity, field);
} else throw e;
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public Object loadFieldMultiple(AccessRegistrarIF registrar, Collection<IdentityIF> identities,
IdentityIF current, Class<?> type, int field) {
try {
if (debug) {
if (current == null)
log.debug("Loading field: " + field + " batch: " + (identities == null ? 0 : identities.size()));
else
log.debug("Loading field: " + field + " identity: " + current + " and " +
(identities == null ? 0 : identities.size()) + " others");
}
try {
return getHandler(type).loadFieldMultiple(registrar, identities, current, field);
} catch (IdentityNotFoundException e) {
throw e;
} catch (Exception e) {
if (readonly && isSQLException(e)) {
// if read-only, reset connection and try again
resetConnection();
log.warn(getId() + ": Connection seems to be down. Resetting read-only connection before second attempt (loadFieldMultiple).", e);
return getHandler(type).loadFieldMultiple(registrar, identities, current, field);
} else throw e;
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public void createObject(ObjectAccessIF oaccess, Object object) {
try {
if (debug)
log.debug(getId() + ": Creating object " + oaccess.getIdentity(object));
getHandler(oaccess.getType(object)).create(oaccess, object);
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public void deleteObject(ObjectAccessIF oaccess, Object object) {
try {
if (debug)
log.debug(getId() + ": Deleting object " + oaccess.getIdentity(object));
getHandler(oaccess.getType(object)).delete(oaccess, object);
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public void storeDirty(ObjectAccessIF oaccess, Object object) {
try {
if (debug)
log.debug(getId() + ": Storing dirty object " + oaccess.getIdentity(object));
getHandler(oaccess.getType(object)).storeDirty(oaccess, object);
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
void needsFlushing(FlushableIF handler) {
flushable.add(handler);
}
// -----------------------------------------------------------------------------
// Queries
// -----------------------------------------------------------------------------
public QueryIF createQuery(String name, ObjectAccessIF oaccess, AccessRegistrarIF registrar) {
return storage.createQuery(name, this, oaccess, registrar);
}
public QueryIF createQuery(JDOQuery jdoquery, ObjectAccessIF oaccess, AccessRegistrarIF registrar, boolean lookup_identities) {
return storage.createQuery(jdoquery, this, oaccess, registrar, lookup_identities);
}
// -----------------------------------------------------------------------------
// Identity generator
// -----------------------------------------------------------------------------
public IdentityIF generateIdentity(Class<?> type) {
return storage.generateIdentity(type);
}
}