dnet-core/dnet-information-service/src/main/java/eu/dnetlib/xml/database/exist/ExistDatabase.java

636 lines
15 KiB
Java

package eu.dnetlib.xml.database.exist; // NOPMD
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exist.collections.CollectionConfiguration;
import org.exist.util.DatabaseConfigurationException;
import org.exist.xmldb.DatabaseImpl;
import org.exist.xmldb.DatabaseInstanceManager;
import org.exist.xmldb.EXistResource;
import org.exist.xmldb.XmldbURI;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.Lifecycle;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.*;
import org.xmldb.api.base.Collection;
import org.xmldb.api.modules.CollectionManagementService;
import org.xmldb.api.modules.XPathQueryService;
import eu.dnetlib.miscutils.datetime.DateUtils;
import eu.dnetlib.xml.database.Trigger;
import eu.dnetlib.xml.database.XMLDatabase;
/**
* eXist database wrapper.
*
* @author marko
*
*/
public class ExistDatabase implements XMLDatabase, Lifecycle { // NOPMD by marko
/**
* logger.
*/
private static final Log log = LogFactory.getLog(ExistDatabase.class); // NOPMD
/**
* eXist collection configuration special file.
*/
public static final String COLLECTION_XCONF = "collection.xconf";
/**
* exist xml resource type code.
*/
private static final String XMLRESOURCE = "XMLResource";
/**
* collection name to trigger instance map.
*
* all triggers declared here will be registered at startup.
*
*/
private Map<String, Trigger> triggerConf = new HashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
/**
* eXist database.
*/
private Database database;
/**
* eXist collection.
*/
private Collection root;
/**
* eXist database manager.
*/
private DatabaseInstanceManager manager;
/**
* eXist xpath service.
*/
private XPathQueryService queryService;
/**
* eXist collection manager.
*/
private CollectionManagementService colman;
/**
* eXist configuration file.
*/
private String configFile;
/**
* Directory in which backups are saved.
*/
private String backupDir;
/**
* {@inheritDoc}
*
* @see org.springframework.context.Lifecycle#start()
*/
@Override
public void start() {
log.info("starting database");
try {
if (getDatabase() == null) {
setDatabase(new DatabaseImpl());
getDatabase().setProperty("configuration", getConfigFile());
getDatabase().setProperty("create-database", "true");
}
DatabaseManager.registerDatabase(getDatabase());
setRoot(DatabaseManager.getCollection("xmldb:exist://" + getRootCollection(), "admin", ""));
setManager((DatabaseInstanceManager) getRoot().getService("DatabaseInstanceManager", "1.0"));
setQueryService((XPathQueryService) getRoot().getService("XPathQueryService", "1.0"));
setColman((CollectionManagementService) getRoot().getService("CollectionManagementService", "1.0"));
for (final Entry<String, Trigger> entry : getTriggerConf().entrySet())
registerTrigger(entry.getValue(), entry.getKey());
} catch (final XMLDBException e) {
throw new IllegalStateException("cannot open eXist database", e);
}
}
/**
* helper method.
*
* @param collection
* collection name
* @return an eXist collection
* @throws XMLDBException
* happens
*/
protected Collection getCollection(final String collection) throws XMLDBException {
readLock.lock();
try {
if (!collection.startsWith("/db"))
throw new XMLDBException(0, "collection path should begin with /db");
return database.getCollection("exist://" + collection, "admin", "");
}finally {
readLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#create(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void create(final String name, final String collection, final String content) throws XMLDBException {
writeLock.lock();
try {
if ("".equals(name))
throw new XMLDBException(0, "cannot create a xml file with an empty file name");
Collection col = getCollection(collection);
if (col == null) {
// create parent collections
createCollection(collection, true);
col = getCollection(collection);
}
final Resource res = col.createResource(name, XMLRESOURCE);
res.setContent(content);
col.storeResource(res);
((EXistResource) res).freeResources();
col.close();
}finally {
writeLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#remove(java.lang.String, java.lang.String)
*/
@Override
public boolean remove(final String name, final String collection) throws XMLDBException {
writeLock.lock();
try {
final Collection col = getCollection(collection);
final Resource res = col.getResource(name);
if (res == null)
return false;
col.removeResource(res);
col.close();
return true;
} finally {
writeLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#update(java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void update(final String name, final String collection, final String content) throws XMLDBException {
writeLock.lock();
try{
final Collection col = getCollection(collection);
final Resource res = col.getResource(name);
if (res == null) {
throw new XMLDBException(0, "resource doesn't exist");
}
res.setContent(content);
col.storeResource(res);
((EXistResource) res).freeResources();
col.close();
} finally {
writeLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#read(java.lang.String, java.lang.String)
*/
@Override
public String read(final String name, final String collection) throws XMLDBException {
readLock.lock();
try {
Resource res = null;
final Collection coll = getCollection(collection);
try {
if (coll == null)
return null;
res = coll.getResource(name);
if (res != null)
return (String) res.getContent();
return null;
} finally {
if (res != null)
((EXistResource) res).freeResources();
coll.close();
}
}finally {
readLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#xquery(java.lang.String)
*/
@Override
public Iterator<String> xquery(final String query) throws XMLDBException {
readLock.lock();
try {
final ResourceSet result = getQueryService().query(query);
if (result == null)
return null;
final ResourceIterator iterator = result.getIterator();
return new Iterator<String>() {
@Override
public boolean hasNext() {
try {
return iterator.hasMoreResources();
} catch (XMLDBException e) {
throw new RuntimeException("Error while getting next element", e);
}
}
@Override
public String next() {
Resource res = null;
try {
res = iterator.nextResource();
return (String) res.getContent();
} catch (XMLDBException e) {
throw new RuntimeException("Error while getting next element", e);
} finally {
if (res != null)
try {
((EXistResource) res).freeResources();
} catch (XMLDBException e) {
log.error("error on free resource");
}
}
}
};
} finally {
readLock.unlock();
}
}
@Override
public void xupdate(final String query) throws XMLDBException {
writeLock.lock();
try{
getQueryService().query(query);
} finally {
writeLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see org.springframework.context.Lifecycle#stop()
*/
@Override
public void stop() {
// no operation
try {
getManager().shutdown();
DatabaseManager.deregisterDatabase(database);
} catch (final XMLDBException e) {
log.fatal("cannot close database", e);
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#collectionExists(java.lang.String)
*/
@Override
public boolean collectionExists(final String collection) throws XMLDBException {
Collection col = null;
try{
col = getCollection(collection);
return col != null;
} finally {
if (col!=null)
col.close();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#createCollection(java.lang.String)
*/
@Override
public void createCollection(final String collection) throws XMLDBException {
writeLock.lock();
try {
createCollection(collection, false);
}finally {
writeLock.unlock();
}
}
private void createCollection(final String collection, final boolean recursive) throws XMLDBException {
if (recursive) {
final XmldbURI uri = XmldbURI.create(collection).removeLastSegment();
if (!collectionExists(uri.toString()))
createCollection(uri.toString(), true);
}
getColman().createCollection(collection);
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#removeCollection(java.lang.String)
*/
@Override
public void removeCollection(final String collection) throws XMLDBException {
writeLock.lock();
try {
getColman().removeCollection(collection);
}finally {
writeLock.unlock();
}
}
public String getConfigFile() {
return configFile;
}
public void setConfigFile(final String configFile) {
this.configFile = configFile;
}
@Override
public String getBackupDir() {
return backupDir;
}
@Required
public void setBackupDir(final String backupDir) {
this.backupDir = backupDir;
}
/**
* {@inheritDoc}
*
* @see org.springframework.context.Lifecycle#isRunning() useless contract with spring.
*
*/
@Override
public boolean isRunning() {
return false;
}
protected Database getDatabase() {
return database;
}
protected void setDatabase(final Database database) {
this.database = database;
}
protected Collection getRoot() {
return root;
}
protected void setRoot(final Collection root) {
this.root = root;
}
protected DatabaseInstanceManager getManager() {
return manager;
}
protected void setManager(final DatabaseInstanceManager manager) {
this.manager = manager;
}
protected XPathQueryService getQueryService() {
return queryService;
}
protected void setQueryService(final XPathQueryService queryService) {
this.queryService = queryService;
}
protected CollectionManagementService getColman() {
return colman;
}
protected void setColman(final CollectionManagementService colman) {
this.colman = colman;
}
@Override
public String getRootCollection() {
return "/db";
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#listChildCollections(java.lang.String)
*/
@Override
public List<String> listChildCollections(final String collection) throws XMLDBException {
readLock.lock();
try {
final Collection col = getCollection(collection);
if (col == null)
return new ArrayList<>();
return Arrays.asList(col.listChildCollections());
} finally {
readLock.unlock();
}
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#list(java.lang.String)
*/
@Override
public List<String> list(final String collection) throws XMLDBException {
readLock.lock();
try {
final Collection col = getCollection(collection);
if(col == null)
return new ArrayList();
return Arrays.asList(col.listResources());
} finally {
readLock.unlock();
}
}
/**
* sets an underlying eXist trigger class for a given collection.
*
* @param triggerClass
* exist trigger class
* @param collection
* collection name
* @param events
* list of event names
* @param parameters
* parameter map
* @throws XMLDBException
* happens
*/
void setExistTrigger(final Class<?> triggerClass, final String collection, final List<String> events, final Map<String, String> parameters)
throws XMLDBException {
// Arrays.asList(new String[] { "store", "update", "delete" }
final StringBuilder conf = new StringBuilder();
conf.append("<exist:collection xmlns:exist=\"http://exist-db.org/collection-config/1.0\"><exist:triggers>");
final String className = triggerClass.getCanonicalName(); // PMD
conf.append("<exist:trigger event=\"store,update,remove\" class=\"" + className + "\">");
if (parameters != null)
for (final Entry<String, String> entry : parameters.entrySet())
conf.append("<exist:parameter name=\"" + entry.getKey() + "\" value=\"" + entry.getValue() + "\"/>");
conf.append("</exist:trigger>");
conf.append("</exist:triggers></exist:collection>");
log.info(conf.toString());
createCollection("/db/system/config" + collection, true);
create(CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI.toString(), "/db/system/config" + collection, conf.toString());
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#registerTrigger(eu.dnetlib.xml.database.Trigger, java.lang.String)
*/
@Override
public void registerTrigger(final Trigger trigger, final String collection) throws XMLDBException {
final Map<String, String> params = new HashMap<String, String>();
params.put("triggerName", trigger.getName());
ExistTriggerRegistry.defaultInstance().registerTrigger(trigger.getName(), trigger);
setExistTrigger(DelegatingDiffTrigger.class, collection, Arrays.asList("store", "update", "delete" ), params);
}
public Map<String, Trigger> getTriggerConf() {
return triggerConf;
}
public void setTriggerConf(final Map<String, Trigger> triggerConf) {
this.triggerConf = triggerConf;
}
/**
* {@inheritDoc}
*
* @see eu.dnetlib.xml.database.XMLDatabase#backup()
*/
@Override
public String backup() throws XMLDBException, DatabaseConfigurationException {
log.info("Starting backup...");
readLock.lock();
try {
verifyBackupDir();
String seq = (new SimpleDateFormat("yyyyMMdd-HHmm")).format(new Date());
ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(backupDir + "/data-" + seq + ".zip"));
FileWriter logFile = new FileWriter(backupDir + "/report-" + seq + ".log");
logFile.write("Backup started at: " + DateUtils.now_ISO8601() + "\n\n");
backup(getRoot().getName(), zip, logFile);
logFile.write("\nBackup finished at: " + DateUtils.now_ISO8601() + "\n");
logFile.flush();
logFile.close();
zip.flush();
zip.close();
log.info("Backup finished");
return backupDir;
} catch (final Exception e) {
log.error("Backup failed", e);
throw new XMLDBException(0, "cannot backup", e);
}
finally {
readLock.unlock();
}
}
private void verifyBackupDir() {
File d = new File(backupDir);
if (!d.exists()) d.mkdirs();
}
private void backup(String coll, ZipOutputStream zip, FileWriter logFile) throws XMLDBException, IOException {
readLock.lock();
logFile.write("COLLECTION: " + coll + "\n");
log.info("Backup of collection " + coll);
try {
for (String file : list(coll)) {
zip.putNextEntry(new ZipEntry(coll + "/" + file + ".xml"));
Resource resource = getCollection(coll).getResource(file);
zip.write(resource.getContent().toString().getBytes());
zip.closeEntry();
}
for (String c : listChildCollections(coll)) {
backup(coll + "/" + c, zip, logFile);
}
}finally {
readLock.unlock();
}
}
}