// Copyright (c) 2001 Dustin Sallings <dustin@spy.net>
package net.spy.db;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import net.spy.SpyObject;
import net.spy.util.IdentityEqualifier;
import net.spy.util.SpyConfig;
/**
* Transactional object saver.
*/
public class Saver extends SpyObject {
private static final int MAX_RECURSION_DEPTH=100;
private final SaveContext context;
private final SpyConfig config;
private int rdepth=0;
// Make sure we don't deal with the same object more than once
private final Set<IdentityEqualifier> listedObjects;
private final ConnectionSource connSrc;
private Connection conn=null;
/**
* Get an instance of Saver with the given database config.
*/
public Saver(SpyConfig conf) {
this(conf, null);
}
/**
* Get an instance of saver with the given database config and context.
*/
public Saver(SpyConfig conf, SaveContext ctx) {
super();
context = ctx == null ? new SaveContext() : ctx;
config=conf;
ConnectionSourceFactory csf=ConnectionSourceFactory.getInstance();
connSrc=csf.getConnectionSource(conf);
listedObjects=new HashSet<IdentityEqualifier>();
}
/**
* Save this Savabale and everything it contains at the default isolation
* level.
*/
public void save(Savable o) throws SaveException {
save(o, null);
}
/**
* Save this Savabale and everything it contains.
*/
public void save(Savable o, Integer isoLevel) throws SaveException {
boolean complete=false;
listedObjects.clear();
int oldIsolationLevel=0;
getLogger().debug(
"Beginning save transaction %s with isolation level %s",
getSessId(), isoLevel);
try {
conn=connSrc.getConnection(config);
if(isoLevel != null) {
oldIsolationLevel=conn.getTransactionIsolation();
conn.setTransactionIsolation(isoLevel.intValue());
}
conn.setAutoCommit(false);
// Begin recursion
rsave(o);
complete=true;
} catch(SQLException se) {
getLogger().error("Exception saving object", se);
throw new SaveException("Error saving object", se);
} catch(RuntimeException e) {
getLogger().error("Runtime exception saving object", e);
throw e;
} catch(Error e) {
getLogger().error("Error saving object", e);
throw e;
} finally {
// figure out whether we need to commit or roll back
if(conn!=null) {
if(complete==false) {
try {
conn.rollback();
} catch(SQLException se) {
getLogger().warn("Problem rolling back.", se);
}
} else {
try {
conn.commit();
} catch(SQLException se) {
throw new SaveException("Error committing", se);
}
}
// Reset autocommit state
try {
conn.setAutoCommit(true);
} catch(SQLException sqe) {
getLogger().warn("Problem resetting autocommit.", sqe);
}
// Reset the isolation level
if(isoLevel != null) {
try {
conn.setTransactionIsolation(oldIsolationLevel);
} catch(SQLException sqe) {
getLogger().warn("Problem resetting isolation level.",
sqe);
}
}
} // Dealt with opened connection
// Return a connection to the pool
if(conn != null) {
connSrc.returnConnection(conn);
}
}
// Inform all of the TransactionListener objects that the
// transaction is complete.
for(IdentityEqualifier ie : listedObjects) {
if(ie.get() instanceof TransactionListener) {
TransactionListener tl=(TransactionListener)ie.get();
tl.transactionCommited();
}
} // end looking at all the listeners.
getLogger().debug("Save transaction %s complete", getSessId());
}
private String getSessId() {
return(Integer.toHexString(context.getId()));
}
// Deal with individual saves.
private void rsave(Savable o) throws SaveException, SQLException {
rdepth++;
checkRecursionDepth();
// Only go through the savables if we haven't gone through the
// savables for this exact object
IdentityEqualifier ie=new IdentityEqualifier(o);
if(!listedObjects.contains(ie)) {
// Add this to the set to keep us from doing it again
listedObjects.add(ie);
// Go through the preSavables
saveLoop(o, o.getPreSavables(context));
// Save this object if it needs saving.
if(o.isNew() || o.isModified()) {
// Log the pre-save
if(getLogger().isDebugEnabled()) {
getLogger().debug("Saving %s in %s",
dbgString(o), getSessId());
}
// Perform the actual save
o.save(conn, context);
// Log the post save
if(getLogger().isDebugEnabled()) {
getLogger().debug("Completed saving %s in %s",
dbgString(o), getSessId());
}
} else {
if(getLogger().isDebugEnabled()) {
getLogger().debug("Not saving %s in %s (not modified)",
dbgString(o), getSessId());
}
}
// Get the post savables
saveLoop(o, o.getPostSavables(context));
} // Haven't seen this object
rdepth--;
}
private void checkRecursionDepth() throws SaveException {
if(rdepth>MAX_RECURSION_DEPTH) {
throw new SaveException("Recursing too deep! Max depth is "
+ MAX_RECURSION_DEPTH);
}
}
// make a pleasant way to print out the debug strings.
private String dbgString(Object o) {
StringBuilder sb=new StringBuilder(80);
if(o == null) {
sb.append("<null>");
} else {
sb.append("{");
sb.append(o.getClass().getName());
sb.append("@");
sb.append(Integer.toHexString(System.identityHashCode(o)));
String s=o.toString();
sb.append(" - ");
if(s.length() < 64) {
sb.append(s);
} else {
sb.append(s.substring(0, 63));
}
sb.append("}");
}
return(sb.toString());
}
// Loop through a Collection of savables, passing each to rsave(). If
// only java supported functional programming...
private void saveLoop(Savable o, Collection<?> name)
throws SaveException, SQLException {
rdepth++;
checkRecursionDepth();
if(name!=null) {
for(Object tmpo : name) {
if(tmpo==null) {
throw new NullPointerException("Got a null object from "
+ o + " (" + dbgString(o) + ")");
}
// Dispatch based on type
if(tmpo instanceof Savable) {
rsave((Savable)tmpo);
} else if(tmpo instanceof Collection<?>) {
saveLoop(o, (Collection<?>)tmpo);
} else {
throw new SaveException(
"Invalid object type found in save tree: "
+ tmpo.getClass() + " from " + o
+ " (" + dbgString(o) + ")");
}
} // iterator loop
} // got a collection
rdepth--;
} // saveLoop()
}