/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/datastore/xml/JDOMDocumentDriver.java,v 1.8 2003/08/14 05:46:41 wbiggs Exp $
This file is part of XORM.
XORM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
XORM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with XORM; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.xorm.datastore.xml;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import org.xorm.datastore.Row;
import org.xorm.datastore.Column;
import org.xorm.datastore.Table;
import org.xorm.datastore.DataFetchGroup;
import org.xorm.datastore.DatastoreDriver;
import org.xorm.datastore.DriverException;
import org.xorm.query.Condition;
import org.xorm.query.Selector;
import org.xorm.query.SimpleCondition;
import org.xorm.util.TypeConverter;
import org.jdom.Document;
import org.jdom.Element;
/**
* A datastore driver that uses an XML document as the datastore.
* Datastore XML descriptor files need to specify a column named "."
* as the primary key column; table names should match element names.
* Data column names should be specified in a limited XPath notation.
* Examples are "@name", "description/text()", and ".." for a parent
* reference.
*
* This class relies on the transactional mechanics of the DocumentHolder
* class. At the beginning of each transaction, it acquires a document
* by calling checkout(); upon commit (but not rollback) it calls
* checkin().
*
* @author Wes Biggs
*/
public class JDOMDocumentDriver implements DatastoreDriver {
private DocumentHolder documentHolder;
private Document document;
private boolean readOnly;
private boolean onlyDidReads;
/**
* Sets the data source, which must be an instance of DocumentHolder.
*/
public JDOMDocumentDriver(DocumentHolder documentHolder) {
this.documentHolder = documentHolder;
}
/**
* Begins a transaction by calling checkout on the DocumentHolder.
*/
public void begin(boolean readOnly) {
document = documentHolder.checkout();
this.readOnly = readOnly;
onlyDidReads = true;
}
/**
* Calls checkin() on the DocumentHolder if any write operations
* were called during the transaction.
*/
public void commit() throws DriverException {
if (!onlyDidReads) {
documentHolder.checkin(document);
}
document = null; // reacquire in next transaction
}
public void rollback() {
document = null; // reacquire in next transaction
}
// CRUD methods
public void create(Row row) {
onlyDidReads = false;
Table table = row.getTable();
Column primaryKey = table.getPrimaryKey();
Element element = new Element(table.getName());
row.setValue(primaryKey, element);
Iterator it = table.getColumns().iterator();
while (it.hasNext()) {
Column c = (Column) it.next();
setValue(element, c, row.getValue(c), true);
}
// If the newly created element is unattached at this point,
// it is not contained within any other element tag, and therefore
// it must be added under the document root.
if (element.getParent() == null && !element.isRootElement()) {
document.getRootElement().addContent(element);
}
}
public void update(Row row) {
onlyDidReads = false;
Table table = row.getTable();
Column primaryKey = table.getPrimaryKey();
Element element = (Element) row.getValue(primaryKey);
Iterator it = table.getColumns().iterator();
while (it.hasNext()) {
Column c = (Column) it.next();
if (row.isDirty(c)) {
setValue(element, c, row.getValue(c), false);
}
}
}
public void delete(Row row) {
onlyDidReads = false;
Table table = row.getTable();
Column primaryKey = table.getPrimaryKey();
Element element = (Element) row.getValue(primaryKey);
element.detach();
// TODO: deal with cascaded delete ramifications on the cache
}
public Collection select(Selector selector, Set extraRows) {
Condition condition = selector.getCondition();
Table table = selector.getTable();
Collection xmlResults = null;
if (condition == null) {
xmlResults = new ArrayList();
xmlResults.add(deriveValue(document.getRootElement(), table.getName()));
} else if (condition instanceof SimpleCondition) {
SimpleCondition sc = (SimpleCondition) condition;
Column column = sc.getColumn();
Object value = sc.getValue();
// ".." == (Element) means get all children of an element
// with an element name matching the table name.
if ("..".equals(column.getName()) && (value instanceof Element)) {
Element parent = (Element) value;
xmlResults = parent.getChildren(table.getName());
}
}
// Populate the rows
ArrayList rows = new ArrayList();
if (xmlResults == null) return rows; // no results
Iterator i = xmlResults.iterator();
while (i.hasNext()) {
Element element = (Element) i.next();
Row row = new Row(table);
populate(row, element);
rows.add(row);
}
return rows;
}
private void populate(Row row, Element element) {
Iterator j = row.getTable().getColumns().iterator();
while (j.hasNext()) {
Column c = (Column) j.next();
Object value = deriveValue(element, c.getName());
if (value instanceof String) {
// Convert to Java Type
Class javaType = XMLType.forName(c.getType());
if (javaType != null) {
value = TypeConverter.convertToType(value, javaType, c.getFormat());
}
}
row.setValue(c, value);
}
}
public int count(Selector selector) {
return select(selector, null).size();
}
// Path corresponds to a very small subset of abbreviated XPath syntax
private Object deriveValue(Element element, String path) {
int pos = path.lastIndexOf('/');
if (pos != -1) {
element = navigateToElement(element, path, false);
path = path.substring(pos + 1);
}
// attribute
if (path.startsWith("@")) {
return element.getAttributeValue(path.substring(1));
}
// text node
if ("text()".equals(path)) {
return element.getTextTrim();
}
if (".".equals(path)) {
return element;
} else if ("..".equals(path)) {
return element.getParent();
} else {
return element.getChild(path);
}
}
/**
* Returns the Element indicated by the subpath (everything up to
* the last '/').
*/
private Element navigateToElement(Element element, String path, boolean create) {
int pos = path.indexOf('/');
String pathTail = null;
if (pos != -1) {
pathTail = path.substring(pos + 1);
path = path.substring(0, pos);
if ("..".equals(path)) {
element = element.getParent();
} else {
Element parent = element;
element = element.getChild(path);
if (create && (element == null)) {
element = new Element(path);
parent.addContent(element);
}
}
return navigateToElement(element, pathTail, create);
}
return element;
}
private void setValue(Element element, Column c, Object value, boolean create) {
String path = c.getName();
if (!(value instanceof Element)) {
// Convert value to String
value = TypeConverter.convertToType(value, String.class, c.getFormat());
}
int pos = path.lastIndexOf('/');
if (pos != -1) {
element = navigateToElement(element, path, create);
path = path.substring(pos + 1);
}
// attribute
if (path.startsWith("@")) {
element.setAttribute(path.substring(1), (String) value);
} else if ("text()".equals(path)) {
element.setText((String) value);
} else if (".".equals(path)) {
// self-reference (primary key) -- ignore
} else if ("..".equals(path)) {
Element parent = (Element) value;
element.detach();
parent.addContent(element);
} else {
// Child reference
element.addContent((Element) value);
}
}
}