/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist.sourceforge.net
*
* 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.
*
* $Id$
*/
package org.exist.xupdate;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.collections.triggers.DocumentTrigger;
import org.exist.collections.triggers.DocumentTriggers;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.persistent.DefaultDocumentSet;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.dom.persistent.DocumentSet;
import org.exist.dom.persistent.MutableDocumentSet;
import org.exist.dom.persistent.NodeSet;
import org.exist.dom.persistent.StoredNode;
import org.exist.security.PermissionDeniedException;
import org.exist.source.Source;
import org.exist.source.StringSource;
import org.exist.storage.DBBroker;
import org.exist.storage.XQueryPool;
import org.exist.storage.lock.Lock;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.hashtable.Int2ObjectHashMap;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.Type;
import org.w3c.dom.NodeList;
/**
* Base class for all XUpdate modifications.
*
* @author Wolfgang Meier
*/
public abstract class Modification {
protected final static Logger LOG = LogManager.getLogger(Modification.class);
/** select Statement in the current XUpdate definition;
* defines the set of nodes to which this XUpdate might apply. */
protected String selectStmt = null;
/**
* NodeList to keep track of created document fragments within
* the currently processed XUpdate modification.
* see {@link XUpdateProcessor#contents}
*/
protected NodeList content = null;
protected DBBroker broker;
/** Documents concerned by this XUpdate modification,
* i.e. the set of documents to which this XUpdate might apply. */
protected DocumentSet docs;
protected Map<String, String> namespaces;
protected Map<String, Object> variables;
protected DocumentSet lockedDocuments = null;
protected MutableDocumentSet modifiedDocuments = new DefaultDocumentSet();
protected Int2ObjectHashMap<DocumentTrigger> triggers;
@SuppressWarnings("unused")
private Modification() {}
/**
* Constructor for Modification.
*/
public Modification(DBBroker broker, DocumentSet docs, String selectStmt,
Map<String, String> namespaces, Map<String, Object> variables) {
this.selectStmt = selectStmt;
this.broker = broker;
this.docs = docs;
this.namespaces = new HashMap<String, String>(namespaces);
this.variables = new TreeMap<String, Object>(variables);
this.triggers = new Int2ObjectHashMap<DocumentTrigger>(97);
// DESIGN_QUESTION : wouldn't that be nice to apply selectStmt right here ?
}
/**
* Process the modification. This is the main method that has to be implemented
* by all subclasses.
*
* @param transaction
* @throws PermissionDeniedException
* @throws LockException
* @throws EXistException
* @throws XPathException
*/
public abstract long process(Txn transaction) throws PermissionDeniedException, LockException,
EXistException, XPathException, TriggerException;
public abstract String getName();
public void setContent(NodeList nodes) {
content = nodes;
}
/**
* Evaluate the select expression.
*
* @param docs
* @return The selected nodes.
* @throws PermissionDeniedException
* @throws EXistException
* @throws XPathException
*/
protected NodeList select(DocumentSet docs)
throws PermissionDeniedException, EXistException, XPathException {
final XQuery xquery = broker.getBrokerPool().getXQueryService();
final XQueryPool pool = broker.getBrokerPool().getXQueryPool();
final Source source = new StringSource(selectStmt);
CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source);
XQueryContext context;
if(compiled == null)
{context = new XQueryContext(broker.getBrokerPool());}
else
{context = compiled.getContext();}
context.setStaticallyKnownDocuments(docs);
declareNamespaces(context);
declareVariables(context);
if(compiled == null)
try {
compiled = xquery.compile(broker, context, source);
} catch (final IOException e) {
throw new EXistException("An exception occurred while compiling the query: " + e.getMessage());
}
Sequence resultSeq = null;
try {
resultSeq = xquery.execute(broker, compiled, null);
} finally {
pool.returnCompiledXQuery(source, compiled);
}
if (!(resultSeq.isEmpty() || Type.subTypeOf(resultSeq.getItemType(), Type.NODE)))
{throw new EXistException("select expression should evaluate to a node-set; got " +
Type.getTypeName(resultSeq.getItemType()));}
if (LOG.isDebugEnabled())
{LOG.debug("found " + resultSeq.getItemCount() + " for select: " + selectStmt);}
return (NodeList)resultSeq.toNodeSet();
}
/**
* @param context
* @throws XPathException
*/
protected void declareVariables(XQueryContext context) throws XPathException {
for (final Iterator<Map.Entry<String, Object>> i = variables.entrySet().iterator(); i.hasNext(); ) {
final Map.Entry<String, Object> entry = (Map.Entry<String, Object>) i.next();
context.declareVariable(entry.getKey(), entry.getValue());
}
}
/**
* @param context
*/
protected void declareNamespaces(XQueryContext context) throws XPathException {
Map.Entry<String, String> entry;
for (final Iterator<Map.Entry<String, String>> i = namespaces.entrySet().iterator(); i.hasNext();) {
entry = (Map.Entry<String, String>) i.next();
context.declareNamespace(
entry.getKey(),
entry.getValue());
}
}
/**
* Acquire a lock on all documents processed by this modification. We have
* to avoid that node positions change during the operation.
* feature trigger_update :
* At the same time we leverage on the fact that it's called before
* database modification to call the eventual triggers.
*
* @return The selected document nodes.
*
* @throws LockException
* @throws PermissionDeniedException
* @throws EXistException
* @throws XPathException
* @throws TriggerException
*/
protected final StoredNode[] selectAndLock(Txn transaction)
throws LockException, PermissionDeniedException, EXistException,
XPathException, TriggerException {
final java.util.concurrent.locks.Lock globalLock = broker.getBrokerPool().getGlobalUpdateLock();
globalLock.lock();
try {
final NodeList nl = select(docs);
lockedDocuments = ((NodeSet)nl).getDocumentSet();
// acquire a lock on all documents
// we have to avoid that node positions change
// during the modification
lockedDocuments.lock(broker, true, false);
final StoredNode ql[] = new StoredNode[nl.getLength()];
for (int i = 0; i < ql.length; i++) {
ql[i] = (StoredNode)nl.item(i);
final DocumentImpl doc = ql[i].getOwnerDocument();
// call the eventual triggers
// TODO -jmv separate loop on docs and not on nodes
//prepare Trigger
prepareTrigger(transaction, doc);
}
return ql;
} finally {
globalLock.unlock();
}
}
/**
* Release all acquired document locks;
* feature trigger_update :
* at the same time we leverage on the fact that it's called after
* database modification to call the eventual triggers
* @throws TriggerException
*/
protected final void unlockDocuments(Txn transaction) throws TriggerException
{
if(lockedDocuments == null)
{return;}
//finish Trigger
final Iterator<DocumentImpl> iterator = modifiedDocuments.getDocumentIterator();
while (iterator.hasNext()) {
finishTrigger(transaction, iterator.next());
}
triggers.clear();
modifiedDocuments.clear();
//unlock documents
lockedDocuments.unlock(true);
lockedDocuments = null;
}
/**
* Check if any of the modified documents needs defragmentation.
*
* Defragmentation will take place if the number of split pages in the
* document exceeds the limit defined in the configuration file.
*
* @param docs
*/
protected void checkFragmentation(Txn transaction, DocumentSet docs) throws EXistException {
int fragmentationLimit = -1;
final Object property = broker.getBrokerPool().getConfiguration().getProperty(DBBroker.PROPERTY_XUPDATE_FRAGMENTATION_FACTOR);
if (property != null)
{fragmentationLimit = ((Integer)property).intValue();}
for(final Iterator<DocumentImpl> i = docs.getDocumentIterator(); i.hasNext(); ) {
final DocumentImpl next = i.next();
if(next.getMetadata().getSplitCount() > fragmentationLimit)
{broker.defragXMLResource(transaction, next);}
broker.checkXMLResourceConsistency(next);
}
}
/**
* Fires the prepare function for the UPDATE_DOCUMENT_EVENT trigger for the Document doc
*
* @param transaction The transaction
* @param doc The document to trigger for
*
* @throws TriggerException
*/
private void prepareTrigger(Txn transaction, DocumentImpl doc) throws TriggerException {
final Collection col = doc.getCollection();
final DocumentTrigger trigger = new DocumentTriggers(broker, col);
trigger.beforeUpdateDocument(broker, transaction, doc);
triggers.put(doc.getDocId(), trigger);
}
/**
* Fires the finish function for UPDATE_DOCUMENT_EVENT for the documents trigger
*
* @param transaction The transaction
* @param doc The document to trigger for
*
* @throws TriggerException
*/
private void finishTrigger(Txn transaction, DocumentImpl doc) throws TriggerException {
final DocumentTrigger trigger = triggers.get(doc.getDocId());
if(trigger != null)
{trigger.afterUpdateDocument(broker, transaction, doc);}
}
public String toString() {
// buf.append(XMLUtil.dump(content));
return "<xu:" + getName() + " select=\"" + selectStmt + "\">" + "</xu:" + getName() + ">";
}
}