/* * 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 org.apache.log4j.Logger; import org.exist.EXistException; import org.exist.collections.CollectionConfiguration; import org.exist.collections.CollectionConfigurationException; import org.exist.collections.triggers.DocumentTrigger; import org.exist.collections.triggers.Trigger; import org.exist.collections.triggers.TriggerException; import org.exist.dom.DefaultDocumentSet; import org.exist.dom.DocumentImpl; import org.exist.dom.DocumentSet; import org.exist.dom.MutableDocumentSet; import org.exist.dom.NodeIndexListener; import org.exist.dom.NodeSet; import org.exist.dom.StoredNode; import org.exist.security.PermissionDeniedException; import org.exist.security.xacml.AccessContext; import org.exist.security.xacml.NullAccessContextException; import org.exist.source.Source; import org.exist.source.StringSource; import org.exist.storage.DBBroker; import org.exist.storage.StorageAddress; 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.Constants; 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; import java.io.IOException; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; /** * Base class for all XUpdate modifications. * * @author Wolfgang Meier */ public abstract class Modification { protected final static Logger LOG = Logger.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 namespaces; protected Map variables; protected DocumentSet lockedDocuments = null; protected MutableDocumentSet modifiedDocuments = new DefaultDocumentSet(); protected Int2ObjectHashMap triggers; private AccessContext accessCtx; private Modification() {} /** * Constructor for Modification. */ public Modification(DBBroker broker, DocumentSet docs, String selectStmt, Map namespaces, Map variables) { this.selectStmt = selectStmt; this.broker = broker; this.docs = docs; this.namespaces = new HashMap(namespaces); this.variables = new TreeMap(variables); this.triggers = new Int2ObjectHashMap(97); // DESIGN_QUESTION : wouldn't that be nice to apply selectStmt right here ? } public final void setAccessContext(AccessContext accessCtx) { if(accessCtx == null) throw new NullAccessContextException(); if(this.accessCtx != null) throw new IllegalStateException("Access context can only be set once."); this.accessCtx = accessCtx; } public final AccessContext getAccessContext() { if(accessCtx == null) throw new IllegalStateException("Access context has not been set."); return accessCtx; } /** * 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; 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 { XQuery xquery = broker.getXQueryService(); XQueryPool pool = xquery.getXQueryPool(); Source source = new StringSource(selectStmt); CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source); XQueryContext context; if(compiled == null) context = xquery.newContext(getAccessContext()); else context = compiled.getContext(); context.setStaticallyKnownDocuments(docs); declareNamespaces(context); declareVariables(context); if(compiled == null) try { compiled = xquery.compile(context, source); } catch (IOException e) { throw new EXistException("An exception occurred while compiling the query: " + e.getMessage()); } Sequence resultSeq = null; try { resultSeq = xquery.execute(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 (Iterator i = variables.entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry) i.next(); context.declareVariable(entry.getKey().toString(), entry.getValue()); } } /** * @param context */ protected void declareNamespaces(XQueryContext context) throws XPathException { Map.Entry entry; for (Iterator i = namespaces.entrySet().iterator(); i.hasNext();) { entry = (Map.Entry) i.next(); context.declareNamespace( (String) entry.getKey(), (String) 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 */ protected final StoredNode[] selectAndLock(Txn transaction) throws LockException, PermissionDeniedException, EXistException, XPathException { Lock globalLock = broker.getBrokerPool().getGlobalUpdateLock(); try { globalLock.acquire(Lock.READ_LOCK); 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); StoredNode ql[] = new StoredNode[nl.getLength()]; for (int i = 0; i < ql.length; i++) { ql[i] = (StoredNode)nl.item(i); DocumentImpl doc = (DocumentImpl)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.release(Lock.READ_LOCK); } } /** * 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 */ protected final void unlockDocuments(Txn transaction) { if(lockedDocuments == null) return; //finish Trigger Iterator iterator = modifiedDocuments.getDocumentIterator(); DocumentImpl doc; while (iterator.hasNext()) { doc = (DocumentImpl) iterator.next(); finishTrigger(transaction, doc); } 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; Object property = broker.getBrokerPool().getConfiguration().getProperty(DBBroker.PROPERTY_XUPDATE_FRAGMENTATION_FACTOR); if (property != null) fragmentationLimit = ((Integer)property).intValue(); for(Iterator i = docs.getDocumentIterator(); i.hasNext(); ) { DocumentImpl next = (DocumentImpl) 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 */ private void prepareTrigger(Txn transaction, DocumentImpl doc) { CollectionConfiguration config = doc.getCollection().getConfiguration(broker); DocumentTrigger trigger = null; if(config != null) { //get the UPDATE_DOCUMENT_EVENT trigger try { trigger = (DocumentTrigger)config.newTrigger(Trigger.UPDATE_DOCUMENT_EVENT, broker, doc.getCollection()); } catch (CollectionConfigurationException e) { LOG.debug("An error occurred while initializing a trigger for collection " + doc.getCollection().getURI() + ": " + e.getMessage(), e); } if(trigger != null) { try { //fire trigger prepare trigger.prepare(Trigger.UPDATE_DOCUMENT_EVENT, broker, transaction, doc.getURI(), doc); } catch(TriggerException te) { LOG.debug("Unable to prepare trigger for event UPDATE_DOCUMENT_EVENT: " + te.getMessage()); } catch(Exception e) { LOG.debug("Trigger event UPDATE_DOCUMENT_EVENT for collection: " + doc.getCollection().getURI() + " with: " + doc.getURI() + " " + e.getMessage()); } 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 */ private void finishTrigger(Txn transaction, DocumentImpl doc) { DocumentTrigger trigger = (DocumentTrigger) triggers.get(doc.getDocId()); if(trigger != null) { try { trigger.finish(Trigger.UPDATE_DOCUMENT_EVENT, broker, transaction, doc.getURI(), doc); } catch(Exception e) { LOG.debug("Trigger event UPDATE_DOCUMENT_EVENT for collection: " + doc.getCollection().getURI() + " with: " + doc.getURI() + " " + e.getMessage()); } } } public String toString() { StringBuilder buf = new StringBuilder(); buf.append("<xu:"); buf.append(getName()); buf.append(" select=\""); buf.append(selectStmt); buf.append("\">"); // buf.append(XMLUtil.dump(content)); buf.append("</xu:"); buf.append(getName()); buf.append(">"); return buf.toString(); } final static class IndexListener implements NodeIndexListener { StoredNode[] nodes; public IndexListener(StoredNode[] nodes) { this.nodes = nodes; } /* (non-Javadoc) * @see org.exist.dom.NodeIndexListener#nodeChanged(org.exist.dom.NodeImpl) */ public void nodeChanged(StoredNode node) { final long address = node.getInternalAddress(); for (int i = 0; i < nodes.length; i++) { if (StorageAddress.equals(nodes[i].getInternalAddress(), address)) { nodes[i] = node; } } } /* (non-Javadoc) * @see org.exist.dom.NodeIndexListener#nodeChanged(long, long) */ public void nodeChanged(long oldAddress, long newAddress) { // Ignore the address change // TODO: is this really save? // for (int i = 0; i < nodes.length; i++) { // if (StorageAddress.equals(nodes[i].getInternalAddress(), oldAddress)) { // nodes[i].setInternalAddress(newAddress); // } // } } } final static class NodeComparator implements Comparator { /* (non-Javadoc) * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ public int compare(Object o1, Object o2) { StoredNode n1 = (StoredNode) o1; StoredNode n2 = (StoredNode) o2; if (n1.getInternalAddress() == n2.getInternalAddress()) return Constants.EQUAL; if (n1.getInternalAddress() < n2.getInternalAddress()) return Constants.INFERIOR; else return Constants.SUPERIOR; } } }