Rewrote embedded XML database to make it more dependable:

New records are first written to temporary files and only renamed to their
actual names when the transaction is committed.
This commit is contained in:
hns 2004-02-17 15:13:40 +00:00
parent e512386e9a
commit 6f3d080b34
3 changed files with 188 additions and 77 deletions

View file

@ -19,9 +19,29 @@ package helma.objectmodel;
/** /**
* This interface is kept for databases that are able * This interface is kept for databases that are able
* to run transactions. Transactions were used for the * to run transactions.
* Berkeley database and might be used in other future
* databases, so we leave transactions in.
*/ */
public interface ITransaction { public interface ITransaction {
public final int ADDED = 0;
public final int UPDATED = 1;
public final int DELETED = 2;
/**
* Complete the transaction by making its changes persistent.
*/
public void commit() throws DatabaseException;
/**
* Rollback the transaction, forgetting the changed items
*/
public void abort() throws DatabaseException;
/**
* Adds a resource to the list of resources encompassed by this transaction
*
* @param res the resource to add
* @param status the status of the resource (ADDED|UPDATED|DELETED)
*/
public void addResource(Object res, int status) throws DatabaseException;
} }

View file

@ -92,7 +92,7 @@ public final class NodeManager {
} }
} }
db = new XmlDatabase(dbHome, null, this); db = new XmlDatabase(dbHome, this);
initDb(); initDb();
} }

View file

@ -25,13 +25,14 @@ import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
/** /**
* A simple XML-database * A simple XML-database
*/ */
public final class XmlDatabase implements IDatabase { public final class XmlDatabase implements IDatabase {
private String dbHome;
private File dbBaseDir; private File dbHomeDir;
private NodeManager nmgr; private NodeManager nmgr;
private IDGenerator idgen; private IDGenerator idgen;
@ -43,19 +44,17 @@ public final class XmlDatabase implements IDatabase {
* Creates a new XmlDatabase object. * Creates a new XmlDatabase object.
* *
* @param dbHome ... * @param dbHome ...
* @param dbFilename ...
* @param nmgr ... * @param nmgr ...
* *
* @throws DatabaseException ... * @throws DatabaseException ...
* @throws RuntimeException ... * @throws RuntimeException ...
*/ */
public XmlDatabase(String dbHome, String dbFilename, NodeManager nmgr) public XmlDatabase(String dbHome, NodeManager nmgr)
throws DatabaseException { throws DatabaseException {
this.dbHome = dbHome;
this.nmgr = nmgr; this.nmgr = nmgr;
dbBaseDir = new File(dbHome); dbHomeDir = new File(dbHome);
if (!dbBaseDir.exists() && !dbBaseDir.mkdirs()) { if (!dbHomeDir.exists() && !dbHomeDir.mkdirs()) {
throw new RuntimeException("Couldn't create DB-directory"); throw new RuntimeException("Couldn't create DB-directory");
} }
@ -63,48 +62,47 @@ public final class XmlDatabase implements IDatabase {
} }
/** /**
* * Shut down the database
*/ */
public void shutdown() { public void shutdown() {
// nothing to do
} }
/** /**
* Start a new transaction.
* *
* * @return the new tranaction object
* @return ... * @throws DatabaseException
*
* @throws DatabaseException ...
*/ */
public ITransaction beginTransaction() throws DatabaseException { public ITransaction beginTransaction() throws DatabaseException {
return null; return new Transaction();
} }
/** /**
* committ the given transaction, makint its changes persistent
* *
* * @param txn
* @param txn ... * @throws DatabaseException
*
* @throws DatabaseException ...
*/ */
public void commitTransaction(ITransaction txn) throws DatabaseException { public void commitTransaction(ITransaction txn) throws DatabaseException {
txn.commit();
} }
/** /**
* Abort the given transaction
* *
* * @param txn
* @param txn ... * @throws DatabaseException
*
* @throws DatabaseException ...
*/ */
public void abortTransaction(ITransaction txn) throws DatabaseException { public void abortTransaction(ITransaction txn) throws DatabaseException {
txn.abort();
} }
/** /**
* Get the next free id to use for new objects
* *
* * @return
* @return ... * @throws ObjectNotFoundException
*
* @throws ObjectNotFoundException ...
*/ */
public String nextID() throws ObjectNotFoundException { public String nextID() throws ObjectNotFoundException {
if (idgen == null) { if (idgen == null) {
@ -115,17 +113,15 @@ public final class XmlDatabase implements IDatabase {
} }
/** /**
* Get the id-generator
* *
* * @param txn
* @param txn ... * @return
* * @throws ObjectNotFoundException
* @return ...
*
* @throws ObjectNotFoundException ...
*/ */
public IDGenerator getIDGenerator(ITransaction txn) public IDGenerator getIDGenerator(ITransaction txn)
throws ObjectNotFoundException { throws ObjectNotFoundException {
File file = new File(dbBaseDir, "idgen.xml"); File file = new File(dbHomeDir, "idgen.xml");
this.idgen = IDGenParser.getIDGenerator(file); this.idgen = IDGenParser.getIDGenerator(file);
@ -133,36 +129,39 @@ public final class XmlDatabase implements IDatabase {
} }
/** /**
* Write the id-generator to file
* *
* * @param txn
* @param txn ... * @param idgen
* @param idgen ... * @throws IOException
*
* @throws IOException ...
*/ */
public void saveIDGenerator(ITransaction txn, IDGenerator idgen) public void saveIDGenerator(ITransaction txn, IDGenerator idgen)
throws IOException { throws IOException {
File file = new File(dbBaseDir, "idgen.xml"); File tmp = File.createTempFile("idgen.xml.", ".tmp", dbHomeDir);
IDGenParser.saveIDGenerator(idgen, file); IDGenParser.saveIDGenerator(idgen, tmp);
this.idgen = idgen; this.idgen = idgen;
File file = new File(dbHomeDir, "idgen.xml");
Resource res = new Resource(file, tmp);
txn.addResource(res, ITransaction.ADDED);
} }
/** /**
* Retrieves a Node from the database.
* *
* * @param txn
* @param txn ... * @param kstr
* @param kstr ... * @return
* * @throws IOException
* @return ... * @throws ObjectNotFoundException
* * @throws ParserConfigurationException
* @throws Exception ... * @throws SAXException
* @throws ObjectNotFoundException ...
*/ */
public INode getNode(ITransaction txn, String kstr) public INode getNode(ITransaction txn, String kstr)
throws IOException, ObjectNotFoundException, throws IOException, ObjectNotFoundException,
ParserConfigurationException, SAXException { ParserConfigurationException, SAXException {
File f = new File(dbBaseDir, kstr + ".xml"); File f = new File(dbHomeDir, kstr + ".xml");
if (!f.exists()) { if (!f.exists()) {
throw new ObjectNotFoundException("Object not found for key " + kstr + "."); throw new ObjectNotFoundException("Object not found for key " + kstr + ".");
@ -174,68 +173,160 @@ public final class XmlDatabase implements IDatabase {
return node; return node;
} catch (RuntimeException x) { } catch (RuntimeException x) {
nmgr.app.logEvent("error reading node from XmlDatbase: " + x.toString()); nmgr.app.logError("Error reading " +f+": " + x.toString());
throw new ObjectNotFoundException(x.toString()); throw new ObjectNotFoundException(x.toString());
} }
} }
/** /**
* Write the node to a temporary file.
* *
* * @param txn the transaction we're in
* @param txn ... * @param kstr the node's key
* @param kstr ... * @param node the node to save
* @param node ... * @throws IOException
*
* @throws Exception ...
*/ */
public void saveNode(ITransaction txn, String kstr, INode node) public void saveNode(ITransaction txn, String kstr, INode node)
throws IOException { throws IOException {
XmlWriter writer = null; XmlWriter writer = null;
File file = new File(dbBaseDir, kstr + ".xml"); File tmp = File.createTempFile(kstr + ".xml.", ".tmp", dbHomeDir);
if (encoding != null) { if (encoding != null) {
writer = new XmlWriter(file, encoding); writer = new XmlWriter(tmp, encoding);
} else { } else {
writer = new XmlWriter(file); writer = new XmlWriter(tmp);
} }
writer.setMaxLevels(1); writer.setMaxLevels(1);
writer.write(node);
writer.write((Node) node);
writer.close(); writer.close();
File file = new File(dbHomeDir, kstr+".xml");
Resource res = new Resource(file, tmp);
txn.addResource(res, ITransaction.ADDED);
} }
/** /**
* Marks an element from the database as deleted
* *
* * @param txn
* @param txn ... * @param kstr
* @param kstr ... * @throws IOException
*
* @throws Exception ...
*/ */
public void deleteNode(ITransaction txn, String kstr) public void deleteNode(ITransaction txn, String kstr)
throws IOException { throws IOException {
File f = new File(dbBaseDir, kstr + ".xml"); Resource res = new Resource(new File(dbHomeDir, kstr+".xml"), null);
txn.addResource(res, ITransaction.DELETED);
f.delete();
} }
/** /**
* set the file encoding to use
* *
* * @param encoding the database's file encoding
* @param enc ...
*/ */
public void setEncoding(String encoding) { public void setEncoding(String encoding) {
this.encoding = encoding; this.encoding = encoding;
} }
/** /**
* get the file encoding used by this database
* *
* * @return the file encoding used by this database
* @return ...
*/ */
public String getEncoding() { public String getEncoding() {
return encoding; return encoding;
} }
class Transaction implements ITransaction {
ArrayList writeFiles = new ArrayList();
ArrayList deleteFiles = new ArrayList();
/**
* Complete the transaction by making its changes persistent.
*/
public void commit() throws DatabaseException {
// move through updated/created files and persist them
int l = writeFiles.size();
for (int i=0; i<l; i++) {
Resource res = (Resource) writeFiles.get(i);
try {
if (res.tmpfile.renameTo(res.file)) {
nmgr.app.logEvent(res.tmpfile+" -> "+res.file);
res.tmpfile.delete();
} else {
nmgr.app.logError("*** Error committing "+res.file);
nmgr.app.logError("*** Committed version is in "+res.tmpfile);
}
} catch (SecurityException ignore) {
// shouldn't happen
}
}
// move through deleted files and delete them
l = deleteFiles.size();
for (int i=0; i<l; i++) {
Resource res = (Resource) deleteFiles.get(i);
try {
res.file.delete();
} catch (SecurityException ignore) {
// shouldn't happen
}
}
// clear registered resources
writeFiles.clear();
deleteFiles.clear();
}
/**
* Rollback the transaction, forgetting the changed items
*/
public void abort() throws DatabaseException {
int l = writeFiles.size();
for (int i=0; i<l; i++) {
Resource res = (Resource) writeFiles.get(i);
try {
res.tmpfile.delete();
} catch (SecurityException ignore) {
// shouldn't happen
}
}
// clear registered resources
writeFiles.clear();
deleteFiles.clear();
}
/**
* Adds a resource to the list of resources encompassed by this transaction
*
* @param res the resource to add
* @param status the status of the resource (ADDED|UPDATED|DELETED)
*/
public void addResource(Object res, int status)
throws DatabaseException {
if (status == DELETED) {
deleteFiles.add(res);
} else {
writeFiles.add(res);
}
}
}
/**
* A holder class for two files, the temporary file and the permanent one
*/
class Resource {
File tmpfile;
File file;
public Resource(File file, File tmpfile) {
this.file = file;
this.tmpfile = tmpfile;
}
}
} }