/* * eXist Open Source Native XML Database * Copyright (C) 2001-07 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 * * $Id$ */ package org.exist.versioning; import org.exist.collections.triggers.FilteringTrigger; import org.exist.collections.triggers.TriggerException; import org.exist.collections.Collection; import org.exist.collections.IndexInfo; import org.exist.collections.CollectionConfigurationException; import org.exist.storage.DBBroker; import org.exist.storage.BrokerPool; import org.exist.storage.lock.Lock; import org.exist.storage.txn.Txn; import org.exist.xmldb.XmldbURI; import org.exist.dom.DocumentImpl; import org.exist.dom.QName; import org.exist.security.*; import org.exist.util.LockException; import org.exist.util.serializer.SAXSerializer; import org.exist.util.serializer.SerializerPool; import org.exist.util.serializer.Receiver; import org.exist.xquery.value.DateTimeValue; import org.exist.xquery.XPathException; import org.apache.log4j.Logger; import org.xml.sax.SAXException; import org.xml.sax.Attributes; import org.xml.sax.helpers.AttributesImpl; import javax.xml.transform.OutputKeys; import java.io.IOException; import java.io.File; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.StringWriter; import java.util.Iterator; import java.util.Properties; import java.util.Date; import java.util.Map; import org.exist.dom.BinaryDocument; public class VersioningTrigger extends FilteringTrigger { public final static Logger LOG = Logger.getLogger(VersioningTrigger.class); public final static XmldbURI VERSIONS_COLLECTION = XmldbURI.SYSTEM_COLLECTION_URI.append("versions"); public final static String BASE_SUFFIX = ".base"; public final static String TEMP_SUFFIX = ".tmp"; public final static String DELETED_SUFFIX = ".deleted"; public final static String BINARY_SUFFIX = ".binary"; public final static String XML_SUFFIX = ".xml"; public final static String PARAM_OVERWRITE = "overwrite"; public final static QName ELEMENT_VERSION = new QName("version", StandardDiff.NAMESPACE, StandardDiff.PREFIX); public final static QName ELEMENT_REMOVED = new QName("removed", StandardDiff.NAMESPACE, StandardDiff.PREFIX); public final static QName PROPERTIES_ELEMENT = new QName("properties", StandardDiff.NAMESPACE, StandardDiff.PREFIX); public final static QName ELEMENT_REPLACED_BINARY = new QName("replaced-binary", StandardDiff.NAMESPACE, StandardDiff.PREFIX); public final static QName ATTRIBUTE_REF = new QName("ref"); public final static QName ELEMENT_REPLACED_XML = new QName("replaced-xml", StandardDiff.NAMESPACE, StandardDiff.PREFIX); private final static Object latch = new Object(); private DBBroker broker; private XmldbURI documentPath; private DocumentImpl lastRev = null; private boolean removeLast = false; private Collection vCollection; private DocumentImpl vDoc = null; private int elementStack = 0; private String documentKey = null; private String documentRev = null; private boolean checkForConflicts = false; public void configure(DBBroker broker, Collection parent, Map parameters) throws CollectionConfigurationException { super.configure(broker, parent, parameters); if (parameters != null) { String allowOverwrite = (String) parameters.get(PARAM_OVERWRITE); if (allowOverwrite != null) checkForConflicts = allowOverwrite.equals("false") || allowOverwrite.equals("no"); } LOG.debug("checkForConflicts: " + checkForConflicts); } public void prepare(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl existingDocument) throws TriggerException { this.broker = broker; this.documentPath = documentPath; User activeUser = broker.getUser(); try { broker.setUser(org.exist.security.SecurityManager.SYSTEM_USER); if(event == UPDATE_DOCUMENT_EVENT || event == REMOVE_DOCUMENT_EVENT) { Collection collection = existingDocument.getCollection(); if (collection.getURI().startsWith(VERSIONS_COLLECTION)) return; vCollection = getVersionsCollection(broker, transaction, documentPath.removeLastSegment()); String existingURI = existingDocument.getFileURI().toString(); XmldbURI baseURI = XmldbURI.create(existingURI + BASE_SUFFIX); DocumentImpl baseRev = vCollection.getDocument(broker, baseURI); String vFileName; if (baseRev == null) { vFileName = baseURI.toString(); removeLast = false; } else if (event == REMOVE_DOCUMENT_EVENT) { vFileName = existingURI + DELETED_SUFFIX; removeLast = false; } else { vFileName = existingURI + TEMP_SUFFIX; removeLast = true; } // setReferenced(true) will tell the broker that the document // data is referenced from another document and should not be // deleted when the orignal document is removed. existingDocument.getMetadata().setReferenced(true); if(existingDocument instanceof BinaryDocument) { XmldbURI binUri = XmldbURI.createInternal(vFileName); broker.copyResource(transaction, existingDocument, vCollection, binUri); vDoc = vCollection.getDocument(broker, binUri); } else { vDoc = new DocumentImpl(broker.getBrokerPool(), vCollection, XmldbURI.createInternal(vFileName)); vDoc.copyOf(existingDocument); vDoc.copyChildren(existingDocument); } if (event != REMOVE_DOCUMENT_EVENT) lastRev = vDoc; } } catch (PermissionDeniedException e) { throw new TriggerException("Permission denied in VersioningTrigger: " + e.getMessage(), e); } catch (Exception e) { LOG.warn("Caught exception in VersioningTrigger: " + e.getMessage(), e); } finally { broker.setUser(activeUser); } } public void finish(int event, DBBroker broker, Txn transaction, XmldbURI documentPath, DocumentImpl document) { if (documentPath.startsWith(VERSIONS_COLLECTION)) return; User activeUser = broker.getUser(); try { broker.setUser(org.exist.security.SecurityManager.SYSTEM_USER); if (vDoc != null && !removeLast) { if(!(vDoc instanceof BinaryDocument)) { try { vDoc.getUpdateLock().acquire(Lock.WRITE_LOCK); vCollection.addDocument(transaction, broker, vDoc); broker.storeXMLResource(transaction, vDoc); } catch (LockException e) { LOG.warn("Versioning trigger could not store base document: " + vDoc.getFileURI() + e.getMessage(), e); } finally { vDoc.getUpdateLock().release(Lock.WRITE_LOCK); } } } if (event == STORE_DOCUMENT_EVENT) { try { vCollection = getVersionsCollection(broker, transaction, documentPath.removeLastSegment()); String existingURI = document.getFileURI().toString(); XmldbURI deletedURI = XmldbURI.create(existingURI + DELETED_SUFFIX); lastRev = vCollection.getDocument(broker, deletedURI); if (lastRev == null) { lastRev = vCollection.getDocument(broker, XmldbURI.create(existingURI + BASE_SUFFIX)); removeLast = false; } else removeLast = true; } catch (IOException e) { LOG.warn("Caught exception in VersioningTrigger: " + e.getMessage(), e); } catch (PermissionDeniedException e) { LOG.warn("Permission denied in VersioningTrigger: " + e.getMessage(), e); } } if (lastRev != null || event == REMOVE_DOCUMENT_EVENT) { try{ long revision = newRevision(broker.getBrokerPool()); if (documentPath.isCollectionPathAbsolute()) documentPath = documentPath.lastSegment(); XmldbURI diffUri = XmldbURI.createInternal(documentPath.toString() + '.' + revision); vCollection.setTriggersEnabled(false); StringWriter writer = new StringWriter(); SAXSerializer sax = (SAXSerializer) SerializerPool.getInstance().borrowObject( SAXSerializer.class); Properties outputProperties = new Properties(); outputProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); outputProperties.setProperty(OutputKeys.INDENT, "no"); sax.setOutput(writer, outputProperties); sax.startDocument(); sax.startElement(ELEMENT_VERSION, null); writeProperties(sax, getVersionProperties(revision, documentPath, activeUser)); if(event == REMOVE_DOCUMENT_EVENT) { sax.startElement(ELEMENT_REMOVED, null); sax.endElement(ELEMENT_REMOVED); } else { //Diff if(document instanceof BinaryDocument) { //create a copy of the last Binary revision XmldbURI binUri = XmldbURI.create(diffUri.toString() + BINARY_SUFFIX); broker.copyResource(transaction, document, vCollection, binUri); //Create metadata about the last Binary Version sax.startElement(ELEMENT_REPLACED_BINARY, null); sax.attribute(ATTRIBUTE_REF, binUri.toString()); sax.endElement(ELEMENT_REPLACED_BINARY); } else if(lastRev instanceof BinaryDocument) { //create a copy of the last XML revision XmldbURI xmlUri = XmldbURI.create(diffUri.toString() + XML_SUFFIX); broker.copyResource(transaction, document, vCollection, xmlUri); //Create metadata about the last Binary Version sax.startElement(ELEMENT_REPLACED_XML, null); sax.attribute(ATTRIBUTE_REF, xmlUri.toString()); sax.endElement(ELEMENT_REPLACED_BINARY); } else { //Diff the XML versions Diff diff = new StandardDiff(broker); diff.diff(lastRev, document); diff.diff2XML(sax); } sax.endElement(ELEMENT_VERSION); sax.endDocument(); String editscript = writer.toString(); // System.out.println("documentPath: " + documentPath); // System.out.println(editscript); if (removeLast) { if(lastRev instanceof BinaryDocument) { vCollection.removeBinaryResource(transaction, broker, lastRev.getFileURI()); } else { vCollection.removeXMLResource(transaction, broker, lastRev.getFileURI()); } } IndexInfo info = vCollection.validateXMLResource(transaction, broker, diffUri, editscript); vCollection.store(transaction, broker, info, editscript, false); } } catch (Exception e) { LOG.warn("Caught exception in VersioningTrigger: " + e.getMessage(), e); } finally { vCollection.setTriggersEnabled(true); } } } finally { broker.setUser(activeUser); } } private Properties getVersionProperties(long revision, XmldbURI documentPath, User user) throws XPathException { Properties properties = new Properties(); properties.setProperty("document", documentPath.toString()); properties.setProperty("revision", Long.toString(revision)); properties.setProperty("date", new DateTimeValue(new Date()).getStringValue()); properties.setProperty("user", user.getName()); if (documentKey != null) { properties.setProperty("key", documentKey); } return properties; } public static void writeProperties(Receiver receiver, Properties properties) throws SAXException { receiver.startElement(PROPERTIES_ELEMENT, null); for (Iterator i = properties.keySet().iterator(); i.hasNext();) { String key = (String) i.next(); QName qn = new QName(key, StandardDiff.NAMESPACE, StandardDiff.PREFIX); receiver.startElement(qn, null); receiver.characters(properties.get(key).toString()); receiver.endElement(qn); } receiver.endElement(PROPERTIES_ELEMENT); } private Collection getVersionsCollection(DBBroker broker, Txn transaction, XmldbURI collectionPath) throws IOException, PermissionDeniedException { XmldbURI path = VERSIONS_COLLECTION.append(collectionPath); Collection collection = broker.openCollection(path, Lock.WRITE_LOCK); if (collection == null) { if(LOG.isDebugEnabled()) LOG.debug("Creating versioning collection: " + path); collection = broker.getOrCreateCollection(transaction, path); broker.saveCollection(transaction, collection); } else { transaction.registerLock(collection.getLock(), Lock.WRITE_LOCK); } return collection; } private long newRevision(BrokerPool pool) { String dataDir = (String) pool.getConfiguration().getProperty(BrokerPool.PROPERTY_DATA_DIR); synchronized (latch) { File f = new File(dataDir, "versions.dbx"); long rev = 0; if (f.canRead()) { DataInputStream is = null; try { is = new DataInputStream(new FileInputStream(f)); rev = is.readLong(); } catch (FileNotFoundException e) { LOG.warn("Failed to read versions.dbx: " + e.getMessage(), e); } catch (IOException e) { LOG.warn("Failed to read versions.dbx: " + e.getMessage(), e); } finally { if(is != null) { try { is.close(); } catch(IOException ioe) { LOG.warn("Failed to close InputStream for versions.dbx: " + ioe.getMessage(), ioe); } } } } ++rev; DataOutputStream os = null; try { os = new DataOutputStream(new FileOutputStream(f)); os.writeLong(rev); } catch (FileNotFoundException e) { LOG.warn("Failed to write versions.dbx: " + e.getMessage(), e); } catch (IOException e) { LOG.warn("Failed to write versions.dbx: " + e.getMessage(), e); } finally { if(os != null) { try { os.close(); } catch(IOException ioe) { LOG.warn("Failed to close OutputStream for versions.dbx: " + ioe.getMessage(), ioe); } } } return rev; } } public void startElement(String namespaceURI, String localName, String qname, Attributes attributes) throws SAXException { if (checkForConflicts && isValidating() && elementStack == 0) { for (int i = 0; i < attributes.getLength(); i++) { if (StandardDiff.NAMESPACE.equals(attributes.getURI(i))) { String attrName = attributes.getLocalName(i); if (VersioningFilter.ATTR_KEY.getLocalName().equals(attrName)) documentKey = attributes.getValue(i); else if (VersioningFilter.ATTR_REVISION.getLocalName().equals(attrName)) documentRev = attributes.getValue(i); } } if (documentKey != null && documentRev != null) { LOG.debug("v:key = " + documentKey + "; v:revision = " + documentRev); try { long rev = Long.parseLong(documentRev); if (VersioningHelper.newerRevisionExists(broker, documentPath, rev, documentKey)) { long baseRev = VersioningHelper.getBaseRevision(broker, documentPath, rev, documentKey); LOG.debug("base revision: " + baseRev); throw new TriggerException("Possible version conflict detected for document: " + documentPath); } } catch (XPathException e) { LOG.warn("Internal error in VersioningTrigger: " + e.getMessage(), e); } catch (IOException e) { LOG.warn("Internal error in VersioningTrigger: " + e.getMessage(), e); } catch (NumberFormatException e) { LOG.warn("Illegal revision number in VersioningTrigger: " + documentRev); } } } if (elementStack == 0) { // Remove the versioning attributes which were inserted during serialization. We don't want // to store them in the db AttributesImpl nattrs = new AttributesImpl(); for (int i = 0; i < attributes.getLength(); i++) { if (!StandardDiff.NAMESPACE.equals(attributes.getURI(i))) nattrs.addAttribute(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i), attributes.getType(i), attributes.getValue(i)); } attributes = nattrs; } elementStack++; super.startElement(namespaceURI, localName, qname, attributes); } public void endElement(String namespaceURI, String localName, String qname) throws SAXException { elementStack--; super.endElement(namespaceURI, localName, qname); } public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { if (StandardDiff.NAMESPACE.equals(namespaceURI)) return; super.startPrefixMapping(prefix, namespaceURI); } }