/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-05 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.exist.collections.triggers;
import static org.custommonkey.xmlunit.XMLAssert.*;
import org.exist.TestUtils;
import org.exist.storage.DBBroker;
import org.exist.util.Base64Decoder;
import org.exist.xmldb.EXistResource;
import org.exist.xmldb.IndexQueryService;
import org.exist.xmldb.DatabaseInstanceManager;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.fail;
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.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.*;
import javax.xml.transform.OutputKeys;
/** class under test : {@link XQueryTrigger}
* @author Pierrick Brihaye <pierrick.brihaye@free.fr>
*/
public class XQueryTriggerTest {
private final static String URI = "xmldb:exist://" + DBBroker.ROOT_COLLECTION;
private final static String TEST_COLLECTION = "testXQueryTrigger";
private final String COLLECTION_CONFIG =
"<exist:collection xmlns:exist='http://exist-db.org/collection-config/1.0'>" +
" <exist:triggers>" +
" <exist:trigger event='store'" +
" class='org.exist.collections.triggers.XQueryTrigger'>" +
" <exist:parameter name='query' " +
" value=\"import module namespace log = 'log' at '" +
URI + "/" + TEST_COLLECTION + "/" + MODULE_NAME + "';" +
"log:log('trigger1')\" />" +
" <exist:parameter name='bindingPrefix' value='log'/>" +
" />" +
" </exist:trigger>" +
" <exist:trigger event='update'" +
" class='org.exist.collections.triggers.XQueryTrigger'>" +
" <exist:parameter name='query' " +
" value=\"import module namespace log = 'log' at '" +
URI + "/" + TEST_COLLECTION + "/" + MODULE_NAME + "';" +
"log:log('trigger2')\" />" +
" <exist:parameter name=\"bindingPrefix\" value=\"log\"/>" +
" />" +
" </exist:trigger>" +
" <exist:trigger event='remove'" +
" class='org.exist.collections.triggers.XQueryTrigger'>" +
" <exist:parameter name=\"query\" value=\"import module namespace log = 'log' at '" +
URI + "/" + TEST_COLLECTION + "/" + MODULE_NAME + "';" +
"log:log('trigger3')\"/>" +
" <exist:parameter name='bindingPrefix' value='log' />" +
" />" +
" </exist:trigger>" +
" </exist:triggers>" +
"</exist:collection>";
private final String EMPTY_COLLECTION_CONFIG =
"<exist:collection xmlns:exist='http://exist-db.org/collection-config/1.0'>" +
"</exist:collection>";
private final static String DOCUMENT_NAME = "test.xml";
private final static String DOCUMENT_CONTENT =
"<test>"
+ "<item id='1'><price>5.6</price><stock>22</stock></item>"
+ "<item id='2'><price>7.4</price><stock>43</stock></item>"
+ "<item id='3'><price>18.4</price><stock>5</stock></item>"
+ "<item id='4'><price>65.54</price><stock>16</stock></item>"
+ "</test>";
/** XUpdate document update specification */
private final static String DOCUMENT_UPDATE =
"<xu:modifications xmlns:xu='http://www.xmldb.org/xupdate' version='1.0'>" +
"<!-- special offer -->" +
"<xu:update select='/test/item[@id = \"3\"]/price'>" +
"15.2"+
"</xu:update>" +
"</xu:modifications>";
private final static String MODIFIED_DOCUMENT_CONTENT =
DOCUMENT_CONTENT.replaceAll("<price>18.4</price>", "<price>15.2</price>");
private final static String BINARY_DOCUMENT_NAME = "1x1.gif";
private final static String BINARY_DOCUMENT_CONTENT = "R0lGODlhAQABAIABAAD/AP///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAAQABAAACAkQBADs=";
/** "log" document that will be updated by the trigger */
private final static String LOG_NAME = "XQueryTriggerLog.xml";
/** initial content of the "log" document */
private final static String EMPTY_LOG = "<events/>";
/** XQuery module implementing the trigger under test */
private final static String MODULE_NAME = "XQueryTriggerLogger.xqm";
/** XQuery module implementing the trigger under test;
* the log() XQuery function will add an <event> element inside <events> element */
private final static String MODULE =
"module namespace log='log'; " +
"import module namespace xmldb='http://exist-db.org/xquery/xmldb'; " +
"declare variable $log:eventType external;" +
"declare variable $log:collectionName external;" +
"declare variable $log:documentName external;" +
"declare variable $log:triggerEvent external;" +
"declare variable $log:document external;" +
"declare function log:log($id as xs:string?) {" +
"let $isLoggedIn := xmldb:login('" + URI + "/" + TEST_COLLECTION + "', 'admin', '') return " +
"xmldb:update(" +
"'" + URI + "/" + TEST_COLLECTION + "', " +
"<xu:modifications xmlns:xu='http://www.xmldb.org/xupdate' version='1.0'>" +
"<xu:append select='/events'>" +
"<xu:element name='event'>" +
"<xu:attribute name='id'>{$id}</xu:attribute>" +
"<xu:attribute name='time'>{current-dateTime()}</xu:attribute>" +
"<xu:attribute name='type'>{$log:eventType}</xu:attribute>" +
"<xu:element name='collectionName'>{$log:collectionName}</xu:element>" +
"<xu:element name='documentName'>{$log:documentName}</xu:element>" +
"<xu:element name='triggerEvent'>{$log:triggerEvent}</xu:element>" +
"<xu:element name='document'>{$log:document}</xu:element>" +
"</xu:element>" +
"</xu:append>" +
"</xu:modifications>" +
")" +
"};";
private static Collection testCollection;
/** XQuery module implementing the invalid trigger under test */
private final static String INVALID_MODULE =
"module namespace log='log'; " +
"import module namespace xmldb='http://exist-db.org/xquery/xmldb'; " +
"declare variable $log:eventType external;" +
"declare variable $log:collectionName external;" +
"declare variable $log:documentName external;" +
"declare variable $log:triggerEvent external;" +
"declare variable $log:document external;" +
"declare function log:log($id as xs:string?) {" +
" undeclared-function-causes-trigger-error()" +
"};";
/** just start the DB and create the test collection */
@BeforeClass
public static void startDB() {
try {
// initialize driver
Class cl = Class.forName("org.exist.xmldb.DatabaseImpl");
Database database = (Database) cl.newInstance();
database.setProperty("create-database", "true");
DatabaseManager.registerDatabase(database);
Collection root = DatabaseManager.getCollection(URI, "admin", null);
CollectionManagementService service = (CollectionManagementService) root
.getService("CollectionManagementService", "1.0");
testCollection = service.createCollection(TEST_COLLECTION);
assertNotNull(testCollection);
} catch (ClassNotFoundException e) {
fail(e.getMessage());
} catch (InstantiationException e) {
fail(e.getMessage());
} catch (IllegalAccessException e) {
fail(e.getMessage());
} catch (XMLDBException e) {
e.printStackTrace();
fail(e.getMessage());
}
}
@AfterClass
public static void shutdownDB() {
TestUtils.cleanupDB();
try {
Collection root = DatabaseManager.getCollection("xmldb:exist://" + DBBroker.ROOT_COLLECTION, "admin", null);
DatabaseInstanceManager mgr = (DatabaseInstanceManager) root.getService("DatabaseInstanceManager", "1.0");
mgr.shutdown();
} catch (XMLDBException e) {
e.printStackTrace();
fail(e.getMessage());
}
testCollection = null;
}
/** create "log" document that will be updated by the trigger,
* and store the XQuery module implementing the trigger under test */
@Before
public void storePreliminaryDocuments() {
try {
XMLResource doc =
(XMLResource) testCollection.createResource(LOG_NAME, "XMLResource" );
doc.setContent(EMPTY_LOG);
testCollection.storeResource(doc);
BinaryResource module =
(BinaryResource) testCollection.createResource(MODULE_NAME, "BinaryResource" );
((EXistResource)module).setMimeType("application/xquery");
module.setContent(MODULE.getBytes());
testCollection.storeResource(module);
} catch (XMLDBException e) {
fail(e.getMessage());
}
}
/** test a trigger fired by storing a Document */
@Test
public void storeDocument() {
ResourceSet result;
try {
// configure the Collection with the trigger under test
IndexQueryService idxConf = (IndexQueryService)
testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
// this will fire the trigger
XMLResource doc =
(XMLResource) testCollection.createResource(DOCUMENT_NAME, "XMLResource" );
doc.setContent(DOCUMENT_CONTENT);
testCollection.storeResource(doc);
// remove the trigger for the Collection under test
idxConf.configureCollection(EMPTY_COLLECTION_CONFIG);
XPathQueryService service = (XPathQueryService) testCollection.getService("XPathQueryService", "1.0");
//TODO : understand why it is necessary !
service.setProperty(OutputKeys.INDENT, "no");
result = service.query("/events/event[@id = 'trigger1']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + DOCUMENT_NAME + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1'][triggerEvent = 'STORE']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1']/document/test");
assertEquals(1, result.getSize());
assertXMLEqual(DOCUMENT_CONTENT, ((XMLResource)result.getResource(0)).getContent().toString());
} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
}
}
/** test a trigger fired by a Document Update */
@Test
public void updateDocument() {
ResourceSet result;
try {
IndexQueryService idxConf = (IndexQueryService)
testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
//TODO : trigger UPDATE events !
XUpdateQueryService update = (XUpdateQueryService) testCollection.getService("XUpdateQueryService", "1.0");
update.updateResource(DOCUMENT_NAME, DOCUMENT_UPDATE);
idxConf.configureCollection(EMPTY_COLLECTION_CONFIG);
XPathQueryService service = (XPathQueryService) testCollection
.getService("XPathQueryService", "1.0");
// this is necessary to compare with MODIFIED_DOCUMENT_CONTENT ; TODO better compare with XML diff tool
service.setProperty(OutputKeys.INDENT, "no");
result = service.query("/events/event[@id = 'trigger2']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger2'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger2'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + DOCUMENT_NAME + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger2'][triggerEvent = 'UPDATE']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger2']/document/test");
assertEquals(2, result.getSize());
assertXMLEqual(DOCUMENT_CONTENT, result.getResource(0).getContent().toString());
assertXMLEqual(MODIFIED_DOCUMENT_CONTENT, result.getResource(1).getContent().toString());
} catch (Exception e) {
fail(e.getMessage());
}
}
/** test a trigger fired by a Document Delete */
@Test
public void deleteDocument() {
ResourceSet result;
try {
IndexQueryService idxConf = (IndexQueryService)
testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
testCollection.removeResource(testCollection.getResource(DOCUMENT_NAME));
idxConf.configureCollection(EMPTY_COLLECTION_CONFIG);
XPathQueryService service = (XPathQueryService) testCollection
.getService("XPathQueryService", "1.0");
service.setProperty(OutputKeys.INDENT, "no");
result = service.query("/events/event[@id = 'trigger3']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + DOCUMENT_NAME + "']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3'][triggerEvent = 'REMOVE']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3']/document/test");
assertEquals(1, result.getSize());
assertXMLEqual(MODIFIED_DOCUMENT_CONTENT, result.getResource(0).getContent().toString());
} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
}
}
/** test a trigger fired by storing a Binary Document */
@Test
public void storeBinaryDocument() {
ResourceSet result;
try {
// configure the Collection with the trigger under test
IndexQueryService idxConf = (IndexQueryService)
testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
// this will fire the trigger
Resource res = testCollection.createResource(BINARY_DOCUMENT_NAME, "BinaryResource");
Base64Decoder dec = new Base64Decoder();
dec.translate(BINARY_DOCUMENT_CONTENT);
res.setContent(dec.getByteArray());
testCollection.storeResource(res);
// remove the trigger for the Collection under test
idxConf.configureCollection(EMPTY_COLLECTION_CONFIG);
XPathQueryService service = (XPathQueryService) testCollection.getService("XPathQueryService", "1.0");
//TODO : understand why it is necessary !
service.setProperty(OutputKeys.INDENT, "no");
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + BINARY_DOCUMENT_NAME + "'][triggerEvent = 'STORE']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger1'][@type = 'finish'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + BINARY_DOCUMENT_NAME + "'][triggerEvent = 'STORE']/document");
assertEquals(1, result.getSize());
assertEquals("<document>" + BINARY_DOCUMENT_CONTENT + "</document>", result.getResource(0).getContent().toString());
}
catch(Exception e)
{
e.printStackTrace();
fail(e.getMessage());
}
}
/** test a trigger fired by a Binary Document Delete */
@Test
public void deleteBinaryDocument() {
ResourceSet result;
try {
IndexQueryService idxConf = (IndexQueryService)
testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
testCollection.removeResource(testCollection.getResource(BINARY_DOCUMENT_NAME));
idxConf.configureCollection(EMPTY_COLLECTION_CONFIG);
XPathQueryService service = (XPathQueryService) testCollection
.getService("XPathQueryService", "1.0");
service.setProperty(OutputKeys.INDENT, "no");
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + BINARY_DOCUMENT_NAME + "'][triggerEvent = 'REMOVE']");
assertEquals(2, result.getSize());
//TODO : consistent URI !
result = service.query("/events/event[@id = 'trigger3'][@type = 'prepare'][collectionName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "'][documentName = '" + DBBroker.ROOT_COLLECTION + "/" + TEST_COLLECTION + "/" + BINARY_DOCUMENT_NAME + "'][triggerEvent = 'REMOVE']/document");
assertEquals(1, result.getSize());
assertEquals("<document>" + BINARY_DOCUMENT_CONTENT + "</document>", result.getResource(0).getContent().toString());
} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
}
}
@Test
public void storeDocument_invalidTriggerForPrepare()
{
//replace the valid trigger with the invalid trigger
try
{
BinaryResource invalidModule = (BinaryResource) testCollection.createResource(MODULE_NAME, "BinaryResource" );
((EXistResource)invalidModule).setMimeType("application/xquery");
invalidModule.setContent(INVALID_MODULE.getBytes());
testCollection.storeResource(invalidModule);
// configure the Collection with the trigger under test
IndexQueryService idxConf = null;
idxConf = (IndexQueryService)testCollection.getService("IndexQueryService", "1.0");
idxConf.configureCollection(COLLECTION_CONFIG);
}
catch(XMLDBException xdbe)
{
fail(xdbe.getMessage());
}
final int max_store_attempts = 10;
int count_prepare_exceptions = 0;
for(int i = 0; i < max_store_attempts; i++)
{
try
{
// this will fire the trigger
XMLResource doc = (XMLResource) testCollection.createResource(DOCUMENT_NAME, "XMLResource");
doc.setContent(DOCUMENT_CONTENT);
testCollection.storeResource(doc);
}
catch(XMLDBException xdbe)
{
if(xdbe.getCause() instanceof TriggerException)
{
if(xdbe.getCause().getMessage().equals(XQueryTrigger.PEPARE_EXCEIPTION_MESSAGE))
{
count_prepare_exceptions++;
}
}
}
}
assertEquals(max_store_attempts, count_prepare_exceptions);
}
}