package org.wyona.yarep.impl.repo.xmldb;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import org.wyona.yarep.core.Path;
import org.wyona.yarep.core.RepositoryException;
import org.wyona.yarep.core.Storage;
import org.wyona.yarep.core.UID;
import org.wyona.commons.io.FileUtil;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
import org.apache.log4j.Logger;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.ResourceIterator;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.Service;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.BinaryResource;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.CollectionManagementService;
import org.xmldb.api.modules.XPathQueryService;
/**
* @author Andreas Wuest
*/
public class XMLDBStorage implements Storage {
private static Logger log = Logger.getLogger(XMLDBStorage.class);
private Credentials mCredentials;
private String mDatabaseURIPrefix;
/**
* XMLDBStorage constructor.
*/
public XMLDBStorage() {}
/**
* XMLDBStorage constructor.
*
* @param aID the repository ID
* @param aRepoConfigFile the repository configuration file
*/
public XMLDBStorage(String aID, File aRepoConfigFile) throws RepositoryException {
Configuration storageConfig;
try {
storageConfig = (new DefaultConfigurationBuilder()).buildFromFile(aRepoConfigFile).getChild("storage", false);
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
readConfig(storageConfig, aRepoConfigFile);
}
/**
* Reads the repository configuration and initialises the database.
*
* @param aStorageConfig the storage configuration
* @param aRepoConfigFile the storage configuration as a raw file
* @throws RepositoryException
*/
public void readConfig(Configuration aStorageConfig, File aRepoConfigFile) throws RepositoryException {
boolean createPrefix;
Configuration repositoryConfig;
Configuration credentialsConfig;
Database database;
File databaseHomeDir;
Service collectionService;
String driverName;
String databaseHome;
String rootCollection;
String pathPrefix;
String databaseAddress;
String databaseName;
String databaseURIPrefix;
/* TODO: replace most log.error() invocations by log.debug().
* Unfortunately, log.debug() produces no output, even if activated
* in the log4.properties. */
// check if we received a storage configuration and a repo config file
if (aStorageConfig == null || aRepoConfigFile == null)
throw new RepositoryException("No storage/repository configuration available.");
try {
// retrieve the database driver name (e.g. "org.apache.xindice.client.xmldb.DatabaseImpl") [mandatory]
driverName = aStorageConfig.getChild("driver").getValue("");
log.error("Specified driver name = \"" + driverName + "\".");
// retrieve the database home (e.g. "../data") [optional]
databaseHome = aStorageConfig.getChild("db-home").getValue(null);
log.error("Specified database home = \"" + databaseHome + "\".");
// retrieve the root collection name (e.g. "db") [mandatory]
rootCollection = aStorageConfig.getChild("root").getValue("");
log.error("Specified root collection = \"" + rootCollection + "\".");
// retrieve the path prefix (e.g. "some/sample/collection") [optional]
pathPrefix = aStorageConfig.getChild("prefix").getValue("");
createPrefix = aStorageConfig.getChild("prefix").getAttributeAsBoolean("createIfNotExists", false);
log.error("Specified collection prefix = \"" + pathPrefix + "\" (create if not exists: \"" + createPrefix + "\").");
// retrieve the name of the database host (e.g. "myhost.domain.com:8080") [optional]
databaseAddress = aStorageConfig.getChild("address").getValue("");
log.error("Specified database address = \"" + databaseAddress + "\".");
// retrieve credentials [optional]
credentialsConfig = aStorageConfig.getChild("credentials", false);
if (credentialsConfig != null) {
mCredentials = new Credentials(credentialsConfig.getChild("username").getValue(""),
credentialsConfig.getChild("password").getValue(""));
log.error("Specified credentials read.");
}
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
// check if the driver name was specified
if (driverName.equals(""))
throw new RepositoryException("Database driver not specified.");
// check if the root collection was specified
if (rootCollection.equals(""))
throw new RepositoryException("Database root collection not specified.");
// register the database with the database manager
try {
database = (Database) Class.forName(driverName).newInstance();
// determine the database location
if (databaseHome != null) {
// resolve the database home relative to the repo config file directory
databaseHomeDir = new File(databaseHome);
if (!databaseHomeDir.isAbsolute()) {
databaseHomeDir = FileUtil.file(aRepoConfigFile.getParent(), databaseHomeDir.toString());
}
log.error("Resolved database home directory = \"" + databaseHomeDir + "\"");
database.setProperty("db-home", databaseHomeDir.toString());
}
// set the database location
DatabaseManager.registerDatabase(database);
databaseName = database.getName();
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
// construct the database URI prefix up to (and inluding) the root collection
databaseURIPrefix = "xmldb:" + databaseName + "://" + databaseAddress + "/" + rootCollection + "/";
// construct the complete database URI prefix including a potential path prefix
if (pathPrefix.equals("")) {
mDatabaseURIPrefix = databaseURIPrefix;
} else {
mDatabaseURIPrefix = databaseURIPrefix + "/" + pathPrefix + "/";
}
log.error("Collection base path = \"" + databaseURIPrefix + "\".");
log.error("Complete collection base path = \"" + mDatabaseURIPrefix + "\".");
// test drive our new database instance
try {
database.acceptsURI(mDatabaseURIPrefix);
} catch (XMLDBException exception) {
log.error(exception);
if (exception.errorCode == org.xmldb.api.base.ErrorCodes.INVALID_URI) {
throw new RepositoryException("The database does not accept the URI prefix \"" + mDatabaseURIPrefix + "\" as valid. Please make sure that the database host address (\"" + databaseAddress + "\") is correct. Original message: " + exception.getMessage(), exception);
} else {
throw new RepositoryException(exception.getMessage(), exception);
}
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
try {
// check if the specified root collection exists
if (getCollection(databaseURIPrefix) == null)
throw new RepositoryException("Specified root collection (\"" + rootCollection + "\") does not exist.");
// check if the complete collection prefix exists
if (getCollectionRelative(null) == null) {
if (createPrefix) {
// create the prefix collection
try {
collectionService = getCollection(databaseURIPrefix).getService("CollectionManagementService", "1.0");
((CollectionManagementService) collectionService).createCollection(pathPrefix);
// re-check if complete collection prefix exists now, we don't want to take any chances here
if (getCollectionRelative(null) == null)
throw new RepositoryException("Specified collection prefix (\"" + pathPrefix + "\") does not exist.");
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException("Failed to create prefix collection (\"" + pathPrefix + "\"). Original message: " + exception.getMessage(), exception);
}
log.error("Created new collection \"" + pathPrefix + "\".");
} else {
// the prefix collection does not exist
throw new RepositoryException("Specified collection prefix (\"" + pathPrefix + "\") does not exist.");
}
}
} catch (Exception exception) {
// something went wrong after registering the database, we have to deregister it now
try {
DatabaseManager.deregisterDatabase(database);
} catch (Exception databaseException) {
log.error(databaseException);
throw new RepositoryException(databaseException.getMessage(), databaseException);
}
/* Rethrow exception. We have to construct a new exception here, because the type system
* doesn't know that only RepositoryExceptions can get to this point (we catch all other
* exceptions above already), and would therefore complain. */
throw new RepositoryException(exception.getMessage(), exception);
}
}
/**
* Returns a Writer to store character data. Creates an XML resource in the database if one of
* the same name and path does not already exist, otherwise overwrites an already existing
* resource. If the already existing resource is of a binary type, the resource is removed,
* and a new XML resource is created.
*
* Call close() on the returned Writer to actually store the data in the database.
*
* Do not use this to write binary data, use getOutputStream instead. This method will
* create a XML character resource. Ignore the deprecated status of this method in the
* super class.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to write to
* @return Writer returns a Writer instance
*/
public Writer getWriter(UID aUID, Path aPath) {
org.wyona.commons.io.Path parentPath;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// obviously, writing means creation of a new resource, so we have to get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
/* For whatever reasons, the Storage interface does not declare this method to throw a
* RepositoryException, therefore we have to catch it here. */
try {
return (new XMLDBStorageWriter(this, parentPath.toString(), null, XMLResource.RESOURCE_TYPE));
} catch (Exception exception) {
return null;
}
}
/**
* Returns an OutputStream to store character data. Creates a binary resource in the database
* if one of the same name and path does not already exist, otherwise overwrites an already
* existing resource. If the already existing resource is of an XML type, the resource is removed,
* and a new binary resource is created.
*
* Call close() on the returned OutputStream to actually store the data in the database.
*
* Do not use this to write character data, use getWriter instead. This method will
* create a binary resource.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to write to
* @return OutputStream returns an OutputStream instance
* @throws RepositoryException
*/
public OutputStream getOutputStream(UID aUID, Path aPath) throws RepositoryException {
org.wyona.commons.io.Path parentPath;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// obviously, writing means creation of a new resource, so we have to get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
return (new XMLDBStorageOutputStream(this, parentPath.toString(), null, BinaryResource.RESOURCE_TYPE));
}
/**
* Returns a Reader to read character data.
*
* Call close() on the returned Reader when you are done reading.
*
* Do not use this to read binary data, use getInputStream instead. If you read
* binary data with this Reader, all data will be converted to characters using
* UTF-8 encoding. Ignore the deprecated status of this method in the super class.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to read from
* @return Reader returns a Reader instance
*/
public Reader getReader(UID aUID, Path aPath) {
org.wyona.commons.io.Path parentPath;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
/* For whatever reasons, the Storage interface does not declare this method to throw a
* RepositoryException, therefore we have to catch it here. */
try {
return (new XMLDBStorageReader(this, parentPath.toString(), aPath.getName()));
} catch (Exception exception) {
return null;
}
}
/**
* Returns an InputStream to read binary data.
*
* Call close() on the returned InputStream when you are done reading.
*
* Do not use this to read character data, use getReader instead. If you read
* XML character data with this InputStream, all data will be converted to bytes
* using UTF-8 encoding. Ignore the deprecated status of this method in the super
* class.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to read from
* @return InputStream returns an InputStream instance
* @throws RepositoryException
*/
public InputStream getInputStream(UID aUID, Path aPath) throws RepositoryException {
org.wyona.commons.io.Path parentPath;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
return (new XMLDBStorageInputStream(this, parentPath.toString(), aPath.getName()));
}
/**
* This repository does not support modification dates.
*/
public long getLastModified(UID aUID, Path aPath) throws RepositoryException {
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
log.warn("This repository does not support modification dates.");
return 0;
}
/**
* Returns the size of a resource.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to get the size
* @return long returns the size of the resource
* @throws RepositoryException throws a RepositoryException if the resource does not exist,
* or another exception occurs
*/
public long getSize(UID aUID, Path aPath) throws RepositoryException {
Collection collection;
org.wyona.commons.io.Path parentPath;
Resource resource;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
if ((collection = getCollectionRelative(parentPath.toString())) == null)
throw new RepositoryException("Requested resource \"" + aPath + "\" does not exist.");
try {
resource = collection.getResource(aPath.getName());
} catch (XMLDBException exception) {
throw new RepositoryException(exception.getMessage(), exception);
}
if (resource == null)
throw new RepositoryException("Requested resource \"" + aPath.getName() + "\" does not exist.");
try {
if (resource.getResourceType().equals(BinaryResource.RESOURCE_TYPE)) {
return ((byte[]) resource.getContent()).length;
} else if (resource.getResourceType().equals(XMLResource.RESOURCE_TYPE)) {
return ((String) resource.getContent()).length();
}
} catch (Exception exception) {
throw new RepositoryException(exception.getMessage(), exception);
}
return 0;
}
/**
* Removes a resource.
*
* Throws a RepositoryException if the resource to remove does not exist.
*
* @param aUID the UID (not used in this implementation)
* @param aPath the path including the resource name of the resource to remove
* @return boolean returns true (TODO: what is this for??)
* @throws RepositoryException throws a RepositoryException if the resource does not exist,
* or another exception occurs
*/
public boolean delete(UID aUID, Path aPath) throws RepositoryException {
Collection collection;
org.wyona.commons.io.Path parentPath;
Resource resource;
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
// get the parent collection
parentPath = aPath.getParent();
log.error("Path to the parent collection = \"" + parentPath.toString() + "\".");
if ((collection = getCollectionRelative(parentPath.toString())) == null)
throw new RepositoryException("Requested resource \"" + aPath + "\" does not exist.");
try {
resource = collection.getResource(aPath.getName());
} catch (XMLDBException exception) {
throw new RepositoryException(exception.getMessage(), exception);
}
if (resource == null)
throw new RepositoryException("Requested resource \"" + aPath.getName() + "\" does not exist.");
try {
collection.removeResource(resource);
} catch (Exception exception) {
throw new RepositoryException(exception.getMessage(), exception);
}
return true;
}
/**
* This repository does not support versioning.
*/
public String[] getRevisions(UID aUID, Path aPath) throws RepositoryException {
log.error("UID = \"" + aUID + "\", path = \"" + aPath + "\".");
log.warn("This repository does not support versioning.");
return null;
}
/**
* Executes a query.
*
* Note that this method is not part of the interface, and the repository therefore
* has to be explicitly casted to org.wyona.yarep.impl.repo.xmldb.XMLDBStorage.
*
* @param aPath the path to the collection against whose subtree the query should be evaluated, or
* null, if the root collection should be assumed
* @param aNamespaceMap a mapping of namespace prefixes (key, as a String) to namespaces (value, as a String)
* as used in the aQuery parameter
* @param aQuery the XPath query (note that the namespace prefixes used have to be declared in the
* aNamespaceMap mapping
* @return java.util.Collection returns a collection of org.xmldb.api.base.Resource's against which the query matched
* @throws RepositoryException if an error occurred retrieving a collection or executing the query
*/
public java.util.Collection executeQuery(Path aPath, Map aNamespaceMap, String aQuery) throws RepositoryException {
ArrayList queryResult;
Collection collection;
Iterator namespaceSetIter;
ResourceIterator resultSetIter;
Map.Entry mapEntry;
ResourceSet resultSet;
XPathQueryService queryService;
log.error("Path = \"" + aPath + "\", query = \"" + aQuery + "\".");
collection = getCollectionRelative((aPath != null ? aPath.toString() : null));
try {
// get the XPathQueryService
queryService = (XPathQueryService) collection.getService("XPathQueryService", "1.0");
// populate namespace map
namespaceSetIter = aNamespaceMap.entrySet().iterator();
while (namespaceSetIter.hasNext()) {
mapEntry = (Map.Entry) namespaceSetIter.next();
queryService.setNamespace((String) mapEntry.getKey(), (String) mapEntry.getValue());
}
// run query
resultSet = queryService.query(aQuery);
// transform result set to collection in order to return it
queryResult = new ArrayList();
resultSetIter = resultSet.getIterator();
while (resultSetIter.hasMoreResources()) {
// add resource names
queryResult.add(resultSetIter.nextResource().getId());
}
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
return queryResult;
}
/**
* Retrieves the collection for the specified collection URI, relative to
* the global collection prefix path.
*
* @param aCollectionURI the relative URI of the collection to retrieve, or null to
* retrieve the global prefix collection
* @return a collection instance for the requested collection or
* null if the collection could not be found
* @throws RepositoryException if an error occurred retrieving the collection (e.g.
* permission was denied)
*/
Collection getCollectionRelative(String aCollectionURI) throws RepositoryException {
return getCollection(constructCollectionURI(aCollectionURI));
}
/**
* Retrieves the collection for the specified collection URI.
*
* @param aCollectionURI the xmldb URI of the collection to retrieve
* @return a collection instance for the requested collection or
* null if the collection could not be found
* @throws RepositoryException if an error occurred retrieving the collection (e.g.
* permission was denied)
*/
private Collection getCollection(String aCollectionURI) throws RepositoryException {
try {
if (mCredentials != null) {
return DatabaseManager.getCollection(aCollectionURI, mCredentials.getUsername(), mCredentials.getPassword());
} else {
return DatabaseManager.getCollection(aCollectionURI);
}
} catch (Exception exception) {
log.error(exception);
throw new RepositoryException(exception.getMessage(), exception);
}
}
private String constructCollectionURI(String aCollectionURI) {
return mDatabaseURIPrefix + "/" + (aCollectionURI != null ? aCollectionURI : "");
}
private class Credentials {
private final String mUsername;
private final String mPassword;
public Credentials(String aUsername, String aPassword) {
mUsername = aUsername;
mPassword = aPassword;
}
public String getUsername() {
return mUsername;
}
public String getPassword() {
return mPassword;
}
}
/**
*
*/
public boolean exists(UID uid, Path path) {
log.error("NOT implemented yet!");
return false;
}
}