/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 8779 $
* $Date: 2008-02-06 15:50:59 +0100 (Mit, 06. Feb 2008) $
*/
package helma.objectmodel.dom;
import helma.objectmodel.*;
import helma.objectmodel.db.NodeManager;
import helma.objectmodel.db.Node;
import helma.framework.core.Application;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.util.ArrayList;
import org.xml.sax.SAXException;
/**
* A simple XML-database
*/
public final class XmlDatabase implements IDatabase {
protected File dbHomeDir;
protected Application app;
protected NodeManager nmgr;
protected XmlIDGenerator idgen;
// character encoding to use when writing files.
// use standard encoding by default.
protected String encoding = null;
/**
* Initializes the database from an application.
* @param app
* @throws DatabaseException
*/
public void init(File dbHome, Application app) throws DatabaseException {
this.app = app;
nmgr = app.getNodeManager();
dbHomeDir = dbHome;
if (!dbHomeDir.exists() && !dbHomeDir.mkdirs()) {
throw new DatabaseException("Can't create database directory "+dbHomeDir);
}
if (!dbHomeDir.canWrite()) {
throw new DatabaseException("No write permission for database directory "+dbHomeDir);
}
File stylesheet = new File(dbHomeDir, "helma.xsl");
// if style sheet doesn't exist, copy it
if (!stylesheet.exists()) {
copyStylesheet(stylesheet);
}
this.encoding = app.getCharset();
// get the initial id generator value
long idBaseValue;
try {
idBaseValue = Long.parseLong(app.getProperty("idBaseValue", "1"));
// 0 and 1 are reserved for root nodes
idBaseValue = Math.max(1L, idBaseValue);
} catch (NumberFormatException ignore) {
idBaseValue = 1L;
}
ITransaction txn = null;
try {
txn = beginTransaction();
try {
idgen = getIDGenerator(txn);
if (idgen.getValue() < idBaseValue) {
idgen.setValue(idBaseValue);
}
} catch (ObjectNotFoundException notfound) {
// will start with idBaseValue+1
idgen = new XmlIDGenerator(idBaseValue);
}
// check if we need to set the id generator to a base value
Node node = null;
try {
getNode(txn, "0");
} catch (ObjectNotFoundException notfound) {
node = new Node("root", "0", "Root", nmgr.safe);
node.setDbMapping(app.getDbMapping("root"));
insertNode(txn, node.getID(), node);
// register node with nodemanager cache
// nmgr.registerNode(node);
}
try {
getNode(txn, "1");
} catch (ObjectNotFoundException notfound) {
node = new Node("users", "1", null, nmgr.safe);
node.setDbMapping(app.getDbMapping("__userroot__"));
insertNode(txn, node.getID(), node);
// register node with nodemanager cache
// nmgr.registerNode(node);
}
commitTransaction(txn);
} catch (Exception x) {
x.printStackTrace();
try {
abortTransaction(txn);
} catch (Exception ignore) {
}
throw (new DatabaseException("Error initializing db"));
}
}
/**
* Try to copy style sheet for XML files to database directory
*/
private void copyStylesheet(File destination) {
InputStream in = null;
FileOutputStream out = null;
byte[] buffer = new byte[1024];
int read;
try {
in = getClass().getResourceAsStream("helma.xsl");
out = new FileOutputStream(destination);
while ((read = in.read(buffer, 0, buffer.length)) > 0) {
out.write(buffer, 0, read);
}
} catch (IOException iox) {
System.err.println("Error copying db style sheet: "+iox);
} finally {
try {
if (out != null)
out.close();
if (in != null)
in.close();
} catch (IOException ignore) {
}
}
}
/**
* Shut down the database
*/
public void shutdown() {
// nothing to do
}
/**
* Start a new transaction.
*
* @return the new tranaction object
* @throws DatabaseException
*/
public ITransaction beginTransaction() throws DatabaseException {
return new XmlTransaction();
}
/**
* committ the given transaction, makint its changes persistent
*
* @param txn
* @throws DatabaseException
*/
public void commitTransaction(ITransaction txn) throws DatabaseException {
if (idgen.dirty) {
try {
saveIDGenerator(txn);
idgen.dirty = false;
} catch (IOException x) {
throw new DatabaseException(x.toString());
}
}
txn.commit();
}
/**
* Abort the given transaction
*
* @param txn
* @throws DatabaseException
*/
public void abortTransaction(ITransaction txn) throws DatabaseException {
txn.abort();
}
/**
* Get the id for the next new object to be stored.
*
* @return the id for the next new object to be stored
* @throws ObjectNotFoundException
*/
public String nextID() throws ObjectNotFoundException {
if (idgen == null) {
getIDGenerator(null);
}
return idgen.newID();
}
/**
* Get the id-generator for this database.
*
* @param txn
* @return the id-generator for this database
* @throws ObjectNotFoundException
*/
public XmlIDGenerator getIDGenerator(ITransaction txn)
throws ObjectNotFoundException {
File file = new File(dbHomeDir, "idgen.xml");
this.idgen = XmlIDGenerator.getIDGenerator(file);
return idgen;
}
/**
* Write the id-generator to file.
*
* @param txn
* @throws IOException
*/
public void saveIDGenerator(ITransaction txn)
throws IOException {
File tmp = File.createTempFile("idgen.xml.", ".tmp", dbHomeDir);
XmlIDGenerator.saveIDGenerator(idgen, tmp);
File file = new File(dbHomeDir, "idgen.xml");
if (file.exists() && !file.canWrite()) {
throw new IOException("No write permission for "+file);
}
Resource res = new Resource(file, tmp);
txn.addResource(res, ITransaction.ADDED);
}
/**
* Retrieves a Node from the database.
*
* @param txn the current transaction
* @param kstr the key
* @return the object associated with the given key
* @throws IOException if an I/O error occurred loading the object.
* @throws ObjectNotFoundException if no object is stored by this key.
*/
public INode getNode(ITransaction txn, String kstr)
throws IOException, ObjectNotFoundException {
File f = new File(dbHomeDir, kstr + ".xml");
if (!f.exists()) {
throw new ObjectNotFoundException("Object not found for key " + kstr);
}
try {
XmlDatabaseReader reader = new XmlDatabaseReader(nmgr);
Node node = reader.read(f);
return node;
} catch (ParserConfigurationException x) {
app.logError("Error reading " +f, x);
throw new IOException(x.toString());
} catch (SAXException x) {
app.logError("Error reading " +f, x);
throw new IOException(x.toString());
}
}
/**
* Save a node with the given key. Writes the node to a temporary file
* which is copied to its final name when the transaction is committed.
*
* @param txn
* @param kstr
* @param node
* @throws java.io.IOException
*/
public void insertNode(ITransaction txn, String kstr, INode node)
throws IOException {
File f = new File(dbHomeDir, kstr + ".xml");
if (f.exists()) {
throw new IOException("Object already exists for key " + kstr);
}
// apart from the above check insertNode() is equivalent to updateNode()
updateNode(txn, kstr, node);
}
/**
* Update a node with the given key. Writes the node to a temporary file
* which is copied to its final name when the transaction is committed.
*
* @param txn
* @param kstr
* @param node
* @throws java.io.IOException
*/
public void updateNode(ITransaction txn, String kstr, INode node)
throws IOException {
XmlWriter writer = null;
File tmp = File.createTempFile(kstr + ".xml.", ".tmp", dbHomeDir);
if (encoding != null) {
writer = new XmlWriter(tmp, encoding);
} else {
writer = new XmlWriter(tmp);
}
writer.setMaxLevels(1);
writer.write(node);
writer.close();
File file = new File(dbHomeDir, kstr+".xml");
if (file.exists() && !file.canWrite()) {
throw new IOException("No write permission for "+file);
}
Resource res = new Resource(file, tmp);
txn.addResource(res, ITransaction.ADDED);
}
/**
* Marks an element from the database as deleted
*
* @param txn
* @param kstr
* @throws IOException
*/
public void deleteNode(ITransaction txn, String kstr)
throws IOException {
Resource res = new Resource(new File(dbHomeDir, kstr+".xml"), null);
txn.addResource(res, ITransaction.DELETED);
}
/**
* set the file encoding to use
*
* @param encoding the database's file encoding
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* get the file encoding used by this database
*
* @return the file encoding used by this database
*/
public String getEncoding() {
return encoding;
}
class XmlTransaction 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 {
// because of a Java/Windows quirk, we have to delete
// the existing file before trying to overwrite it
if (res.file.exists()) {
res.file.delete();
}
// move temporary file to permanent name
if (res.tmpfile.renameTo(res.file)) {
// success - delete tmp file
res.tmpfile.delete();
} else {
// error - leave tmp file and print a message
app.logError("*** Error committing "+res.file);
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);
// delete files enlisted as deleted
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);
// delete tmp files created by this transaction
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;
}
}
class IDGenerator {
}
}