/* * eXist Open Source Native XML Database * Copyright (C) 2001-2016 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 library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.exist.xquery.update; import java.util.Iterator; 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.NodeProxy; import org.exist.dom.persistent.StoredNode; import org.exist.dom.memtree.DocumentBuilderReceiver; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.dom.persistent.NodeHandle; import org.exist.security.PermissionDeniedException; import org.exist.storage.DBBroker; import org.exist.storage.lock.Lock; import org.exist.storage.lock.Lock.LockMode; import org.exist.storage.serializers.Serializer; import org.exist.storage.txn.Txn; import org.exist.util.LockException; import org.exist.util.hashtable.Int2ObjectHashMap; import org.exist.xquery.*; import org.exist.xquery.value.Item; import org.exist.xquery.value.NodeValue; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.SequenceIterator; import org.exist.xquery.value.Type; import org.exist.xquery.value.ValueSequence; import org.w3c.dom.DOMException; import org.w3c.dom.Node; import org.xml.sax.SAXException; /** * @author wolf * */ public abstract class Modification extends AbstractExpression { protected final static Logger LOG = LogManager.getLogger(Modification.class); protected final Expression select; protected final Expression value; protected DocumentSet lockedDocuments = null; protected MutableDocumentSet modifiedDocuments = new DefaultDocumentSet(); protected Int2ObjectHashMap<DocumentTrigger> triggers; /** * @param context */ public Modification(XQueryContext context, Expression select, Expression value) { super(context); this.select = select; this.value = value; this.triggers = new Int2ObjectHashMap<>(10); } public int getCardinality() { return Cardinality.EMPTY; } /* (non-Javadoc) * @see org.exist.xquery.AbstractExpression#returnsType() */ public int returnsType() { return Type.EMPTY; } /* (non-Javadoc) * @see org.exist.xquery.AbstractExpression#resetState() */ public void resetState(boolean postOptimization) { super.resetState(postOptimization); select.resetState(postOptimization); if (value != null) { value.resetState(postOptimization); } } @Override public void accept(ExpressionVisitor visitor) { select.accept(visitor); if (value != null) { value.accept(visitor); } } /* (non-Javadoc) * @see org.exist.xquery.Expression#analyze(org.exist.xquery.Expression, int) */ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { contextInfo.setParent(this); contextInfo.addFlag(IN_UPDATE); select.analyze(contextInfo); if (value != null) { value.analyze(contextInfo); } } /** * Acquire a lock on all documents processed by this modification. * We have to avoid that node positions change during the * operation. * * @param nodes * * @throws LockException * @throws TriggerException */ protected StoredNode[] selectAndLock(Txn transaction, Sequence nodes) throws LockException, PermissionDeniedException, XPathException, TriggerException { final java.util.concurrent.locks.Lock globalLock = context.getBroker().getBrokerPool().getGlobalUpdateLock(); globalLock.lock(); try { lockedDocuments = nodes.getDocumentSet(); // acquire a lock on all documents // we have to avoid that node positions change // during the modification lockedDocuments.lock(context.getBroker(), true, false); final StoredNode ql[] = new StoredNode[nodes.getItemCount()]; for (int i = 0; i < ql.length; i++) { final Item item = nodes.itemAt(i); if (!Type.subTypeOf(item.getType(), Type.NODE)) { throw new XPathException(this, "XQuery update expressions can only be applied to nodes. Got: " + item.getStringValue()); } final NodeValue nv = (NodeValue)item; if (nv.getImplementationType() == NodeValue.IN_MEMORY_NODE) { throw new XPathException(this, "XQuery update expressions can not be applied to in-memory nodes."); } final Node n = nv.getNode(); if (n.getNodeType() == Node.DOCUMENT_NODE) { throw new XPathException(this, "Updating the document object is not allowed."); } ql[i] = (StoredNode) n; final DocumentImpl doc = ql[i].getOwnerDocument(); //prepare Trigger prepareTrigger(transaction, doc); } return ql; } finally { globalLock.unlock(); } } protected Sequence deepCopy(Sequence inSeq) throws XPathException { context.pushDocumentContext(); final MemTreeBuilder builder = context.getDocumentBuilder(); final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(builder); final Serializer serializer = context.getBroker().getSerializer(); serializer.setReceiver(receiver); try { final Sequence out = new ValueSequence(); for (final SequenceIterator i = inSeq.iterate(); i.hasNext(); ) { Item item = i.nextItem(); if (item.getType() == Type.DOCUMENT) { if (((NodeValue)item).getImplementationType() == NodeValue.PERSISTENT_NODE) { final NodeHandle root = (NodeHandle) ((NodeProxy)item).getOwnerDocument().getDocumentElement(); item = new NodeProxy(root); } else { item = (Item)((NodeValue) item).getOwnerDocument().getDocumentElement(); } } if (Type.subTypeOf(item.getType(), Type.NODE)) { if (((NodeValue)item).getImplementationType() == NodeValue.PERSISTENT_NODE) { final int last = builder.getDocument().getLastNode(); final NodeProxy p = (NodeProxy) item; serializer.toReceiver(p, false, false); if (p.getNodeType() == Node.ATTRIBUTE_NODE) {item = builder.getDocument().getLastAttr();} else {item = builder.getDocument().getNode(last + 1);} } else { ((org.exist.dom.memtree.NodeImpl)item).deepCopy(); } } out.add(item); } return out; } catch(final SAXException | DOMException e) { throw new XPathException(this, e.getMessage(), e); } finally { context.popDocumentContext(); } } protected void finishTriggers(Txn transaction) throws TriggerException { final Iterator<DocumentImpl> iterator = modifiedDocuments.getDocumentIterator(); while(iterator.hasNext()) { final DocumentImpl doc = iterator.next(); context.addModifiedDoc(doc); finishTrigger(transaction, doc); } triggers.clear(); } /** * Release all acquired document locks. */ protected void unlockDocuments() { if(lockedDocuments == null) { return; } modifiedDocuments.clear(); //unlock documents lockedDocuments.unlock(true); lockedDocuments = null; } public static void checkFragmentation(XQueryContext context, DocumentSet docs) throws EXistException { int fragmentationLimit = -1; final Object property = context.getBroker().getBrokerPool().getConfiguration().getProperty(DBBroker.PROPERTY_XUPDATE_FRAGMENTATION_FACTOR); if (property != null) { fragmentationLimit = (Integer) property; } checkFragmentation(context, docs, fragmentationLimit); } /** * 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 */ public static void checkFragmentation(XQueryContext context, DocumentSet docs, int splitCount) throws EXistException { final DBBroker broker = context.getBroker(); //if there is no batch update transaction, start a new individual transaction try (final Txn transaction = broker.getBrokerPool().getTransactionManager().beginTransaction()) { for (final Iterator<DocumentImpl> i = docs.getDocumentIterator(); i.hasNext(); ) { final DocumentImpl next = i.next(); if(next.getMetadata().getSplitCount() > splitCount) { Lock lock = next.getUpdateLock(); try { lock.acquire(LockMode.WRITE_LOCK); broker.defragXMLResource(transaction, next); } finally { lock.release(LockMode.WRITE_LOCK); }} broker.checkXMLResourceConsistency(next); } transaction.commit(); } catch (final Exception e) { LOG.error(e, e); } } /** * 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 DBBroker broker = context.getBroker(); final DocumentTrigger trigger = new DocumentTriggers(broker, col); //prepare the trigger trigger.beforeUpdateDocument(context.getBroker(), 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 { //finish the trigger final DocumentTrigger trigger = triggers.get(doc.getDocId()); if(trigger != null) { trigger.afterUpdateDocument(context.getBroker(), transaction, doc); } } /** * Gets the Transaction to use for the update (can be batch or individual) * * @return The transaction */ protected Txn getTransaction() { return context.getBroker().getBrokerPool().getTransactionManager().beginTransaction(); } }