org.xflatdb.xflat.transaction.ThreadContextTransactionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.xflatdb.xflat.transaction.ThreadContextTransactionManager.java

Source

/* 
*   Copyright 2013 Gordon Burgett and individual contributors
*
*   Licensed under the Apache License, Version 2.0 (the "License");
*   you may not use this file except in compliance with the License.
*   You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
*   Unless required by applicable law or agreed to in writing, software
*   distributed under the License is distributed on an "AS IS" BASIS,
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*   See the License for the specific language governing permissions and
*   limitations under the License.
*/
package org.xflatdb.xflat.transaction;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.xflatdb.xflat.XFlatException;
import org.xflatdb.xflat.convert.ConversionException;
import org.xflatdb.xflat.convert.Converter;
import org.xflatdb.xflat.db.EngineBase;
import org.xflatdb.xflat.db.EngineTransactionManager;
import org.xflatdb.xflat.db.XFlatDatabase;
import org.xflatdb.xflat.util.DocumentFileWrapper;

/**
 * A {@link TransactionManager} that uses the current thread as the context for transactions.
 * Each transaction opened by this manager will be bound to the current thread, and
 * the {@link #getTransaction() } method will return the transaction open on the current
 * thread, if any.
 * <p/>
 * This is the default TransactionManager used by XFlat.
 * @author Gordon
 */
public class ThreadContextTransactionManager extends EngineTransactionManager {

    private Map<Long, AmbientThreadedTransactionScope> currentTransactions = new ConcurrentHashMap<>();

    private Map<Long, AmbientThreadedTransactionScope> committedTransactions = new ConcurrentHashMap<>();

    private DocumentFileWrapper journalWrapper;
    private Document transactionJournal = null;

    private Log log = LogFactory.getLog(getClass());

    /**
     * Creates a new ThreadContextTransactionManager, which will manage a mapping
     * of threads to transactions.
     * @param wrapper A wrapper which wraps the file to which this Transaction Manager
     * can save its Transaction Journal, for recovery in case of catastrophic error.
     */
    public ThreadContextTransactionManager(DocumentFileWrapper wrapper) {
        this.journalWrapper = wrapper;
    }

    /**
     * Gets the Id of the current context, which is the current thread's ID.     
     * @return The current thread's ID.
     */
    protected Long getContextId() {
        return Thread.currentThread().getId();
    }

    @Override
    public Transaction getTransaction() {
        TransactionBase tx = currentTransactions.get(getContextId());
        if (tx == null || tx.getOptions().getPropagation() == Propagation.NOT_SUPPORTED)
            return null;

        return tx.getTransaction();
    }

    @Override
    public TransactionScope openTransaction() throws TransactionPropagationException {
        return openTransaction(TransactionOptions.DEFAULT);
    }

    @Override
    public TransactionScope openTransaction(TransactionOptions options) throws TransactionPropagationException {

        AmbientThreadedTransactionScope ret;
        long contextId = getContextId();

        switch (options.getPropagation()) {
        case MANDATORY:
            ret = currentTransactions.get(contextId);
            if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) {
                throw new TransactionPropagationException("propagation MANDATORY, but no current transaction.");
            }
            //use the current transaction, with a wrapper to prevent
            //this instance from closing it prematurely.
            return new WrappingTransactionScope(ret, options.getReadOnly());

        case NESTED:
            throw new UnsupportedOperationException("Nested transactions not yet supported");

        case NEVER:
            ret = currentTransactions.get(contextId);
            if (ret != null && ret.getOptions().getPropagation() != Propagation.NOT_SUPPORTED) {
                throw new TransactionPropagationException("propagation NEVER, but current transaction exists");
            }
            //return a shell object representing the non-transactional operation.
            return new EmptyTransactionScope(options);

        case NOT_SUPPORTED:
            ret = currentTransactions.remove(contextId);
            if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) {
                //we are already operating non-transactionally, just need
                //to return a shell object.
                return new EmptyTransactionScope(options);
            }
            //need to return a shell that will also replace the suspended
            //transaction when it is closed.                
            return new NotSupportedTransaction(ret, options);

        case REQUIRED:
            ret = currentTransactions.get(contextId);
            if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) {
                //no current transaction, create a new one
                //suspending the NOT_SUPPORTED transaction if it exists.
                ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options);
                currentTransactions.put(contextId, ret);
            }

            //use the current transaction, with a wrapper to prevent
            //this instance from closing it prematurely.
            return new WrappingTransactionScope(ret, options.getReadOnly());

        case REQUIRES_NEW:
            //create a new transaction, suspending the current one if it exists.
            ret = currentTransactions.get(contextId);
            ret = new AmbientThreadedTransactionScope(generateNewId(), ret, options);
            currentTransactions.put(contextId, ret);

            //use the current transaction, with a wrapper to prevent
            //this instance from closing it prematurely.
            return new WrappingTransactionScope(ret, options.getReadOnly());

        case SUPPORTS:
            ret = currentTransactions.get(contextId);
            if (ret == null || ret.getOptions().getPropagation() == Propagation.NOT_SUPPORTED) {
                //we are already operating non-transactionally, just need
                //to return a shell object.
                return new EmptyTransactionScope(options);
            }

            //use the current transaction, with a wrapper to prevent
            //this instance from closing it prematurely.
            return new WrappingTransactionScope(ret, options.getReadOnly());

        default:
            throw new UnsupportedOperationException(
                    "Propagation behavior not supported: " + options.getPropagation().toString());
        }
    }

    @Override
    public long isTransactionCommitted(long transactionId) {
        TransactionBase tx = committedTransactions.get(transactionId);
        return tx == null ? -1 : tx.commitId;
    }

    @Override
    public boolean isTransactionReverted(long transactionId) {
        //if we find it in the current transactions, check the transaction
        for (TransactionBase tx : currentTransactions.values()) {
            if (tx.id == transactionId) {
                return tx.isReverted();
            }
        }

        //otherwise it might be in the committed transactions, if so it is not reverted.
        if (committedTransactions.get(transactionId) != null) {
            return false;
        }

        //if we lost it then it's reverted.
        return true;
    }

    @Override
    public long transactionlessCommitId() {
        return generateNewId();
    }

    @Override
    public long getLowestOpenTransaction() {
        long lowest = Long.MAX_VALUE;
        for (TransactionBase tx : currentTransactions.values()) {
            if (tx.id < lowest) {
                lowest = tx.id;
            }
        }

        return lowest;
    }

    @Override
    public void bindEngineToCurrentTransaction(EngineBase engine) {

        TransactionBase tx = currentTransactions.get(getContextId());
        if (tx == null) {
            return;
        }

        //we can get away with just adding it in an unsynchronized context because
        //this is never going to be called at the same time as unbind, since unbind
        //always happens in the context of a commit or revert (which is the same thread as this)
        //or another thread's cleanup after the transaction is closed.
        tx.boundEngines.add(engine);
    }

    @Override
    public synchronized void unbindEngineExceptFrom(EngineBase engine, Collection<Long> transactionIds) {

        Iterator<AmbientThreadedTransactionScope> it = this.committedTransactions.values().iterator();
        while (it.hasNext()) {
            TransactionBase tx = it.next();
            if (transactionIds.contains(tx.id)) {
                continue;
            }

            //try to remove its binding
            tx.boundEngines.remove(engine);

            if (tx.boundEngines.isEmpty()) {
                //remove it from the committed transactions if it is empty.
                it.remove();
            }
        }
    }

    @Override
    public boolean anyOpenTransactions() {
        return !this.currentTransactions.isEmpty();
    }

    private void loadJournal() throws IOException, JDOMException {
        transactionJournal = journalWrapper.readFile();
        if (transactionJournal == null) {
            transactionJournal = new Document();
            transactionJournal.setRootElement(new Element("transactionJournal"));
        }
    }

    private synchronized void commit(AmbientThreadedTransactionScope tx) throws TransactionException {
        //journal the entry so we can recover if catastrophic failure occurs
        TransactionJournalEntry entry = new TransactionJournalEntry();
        entry.txId = tx.id;
        entry.commitId = tx.commitId;
        for (EngineBase e : tx.boundEngines) {
            entry.tableNames.add(e.getTableName());
        }

        Element entryElement = null;
        if (tx.options.isDurable()) {
            //use the transaction journal to ensure durability
            try {
                if (transactionJournal == null) {
                    loadJournal();
                }
                entryElement = toElement.convert(entry);
                transactionJournal.getRootElement().addContent(entryElement);
                journalWrapper.writeFile(transactionJournal);
            } catch (ConversionException | IOException | JDOMException ex) {
                throw new TransactionException("Unable to commit, could not access journal file " + journalWrapper,
                        ex);
            }
        }

        //commit all, and if any fail revert all.
        try {
            for (EngineBase e : tx.boundEngines) {
                if (log.isTraceEnabled())
                    log.trace(String.format("committing transaction %d to table %s", tx.id, e.getTableName()));
                e.commit(tx.getTransaction(), tx.getOptions());
            }
        } catch (Exception ex) {
            try {
                //uncommit
                tx.commitId = -1;
                tx.revert();
            } catch (XFlatException ex2) {
                throw new TransactionException(
                        "Unable to commit, and another error occured during revert: " + ex2.getMessage(), ex);
            }

            //we were able to revert all, no need to keep the transaction in the journal.
            if (tx.options.isDurable()) {
                transactionJournal.getRootElement().removeContent(entryElement);
                try {
                    journalWrapper.writeFile(transactionJournal);
                } catch (IOException ioEx) {
                    //this is not the most important exception
                }
            }

            if (ex instanceof TransactionException)
                throw ex;

            throw new TransactionException("Unable to commit: " + ex.getMessage(), ex);
        }

        //remove it from the transaction journal
        if (tx.options.isDurable()) {
            transactionJournal.getRootElement().removeContent(entryElement);
            try {
                journalWrapper.writeFile(transactionJournal);
            } catch (IOException ex) {
                throw new TransactionException("Unable to commit, could not access journal file " + journalWrapper,
                        ex);
            }
        }

        //we're all committed, so we can finally say so.
        committedTransactions.put(tx.id, tx);
    }

    private void revert(Iterable<EngineBase> boundEngines, long txId, boolean isRecovering) {
        Set<String> failedReverts = null;
        RuntimeException last = null;
        for (EngineBase e : boundEngines) {

            try {
                e.revert(txId, isRecovering);
            } catch (RuntimeException ex) {
                LogFactory.getLog(getClass()).error(ex);
                if (failedReverts == null)
                    failedReverts = new HashSet<>();
                failedReverts.add(e.getTableName());
                last = ex;
            }
        }
        if (failedReverts != null && failedReverts.size() > 0) {
            StringBuilder msg = new StringBuilder(
                    "Unable to revert all bound engines, the data in the following engines may be corrupt: ");
            for (String s : failedReverts) {
                msg.append(s).append(", ");
            }
            //the exceptions we caught were all runtime exceptions, so we are going to throw a runtime exception
            throw new XFlatException(msg.toString(), last);
        }
    }

    @Override
    public void close() {
        //all transactions auto-revert now.
        this.currentTransactions.clear();
    }

    @Override
    public void recover(XFlatDatabase db) {
        //open the journal

        try {
            if (transactionJournal == null) {
                loadJournal();
            }
        } catch (IOException | JDOMException ex) {
            throw new XFlatException("Unable to recover, could not access journal file " + journalWrapper, ex);
        }

        try {
            Iterator<Element> children = transactionJournal.getRootElement().getChildren().iterator();
            while (children.hasNext()) {
                TransactionJournalEntry entry;
                try {
                    entry = fromElement.convert(children.next());
                } catch (ConversionException ex) {
                    //entry is corrupt, remove and continue
                    children.remove();
                    continue;
                }

                List<EngineBase> toRevert = new ArrayList<>();
                for (String table : entry.tableNames) {
                    toRevert.add(db.getEngine(table));
                }

                //revert the transaction in all the engines
                revert(toRevert, entry.txId, true);

                //successful revert - remove the entry
                children.remove();

                //save the journal after each successful revert
                this.journalWrapper.writeFile(transactionJournal);
            }
        } catch (XFlatException | IOException ex) {
            throw new XFlatException("Unable to recover", ex);
        }
    }

    @Override
    public boolean isCommitInProgress(long transactionId) {
        TransactionBase tx = this.currentTransactions.get(transactionId);
        if (tx == null)
            return false;

        //in-progress if the commit ID was assigned and the tx was not yet committed.
        return tx.commitId != -1 && !tx.isCommitted();
    }

    /**
     * The base class for the different types of transactions handled by this
     * transaction manager.  The different implementations are dependent on
     * the propagation level used when the transaction was opened.
     */
    protected abstract class TransactionBase implements TransactionScope {
        protected TransactionOptions options;

        protected AtomicBoolean isCompleted = new AtomicBoolean(false);
        protected AtomicBoolean isRollbackOnly = new AtomicBoolean(false);

        protected volatile boolean isClosed = false;

        protected final long id;

        protected AmbientThreadedTransactionScope suspended;

        //we can get away with this being an unsynchronized HashSet because it will only ever be added to by one
        //thread, and then only so long as the transaction is open, and then will be removed from
        //by a different thread, but only one at a time, synchronized elsewhere, and after all adds are finished.
        final Set<EngineBase> boundEngines = new HashSet<>();

        protected AtomicReference<Set<TransactionListener>> listeners = new AtomicReference<>(null);

        protected long commitId = -1;

        //The transaction representing this transaction scope.
        private final Transaction transaction = new Transaction() {
            @Override
            public long getTransactionId() {
                return id;
            }

            @Override
            public long getCommitId() {
                return commitId;
            }

            @Override
            public boolean isCommitted() {
                return TransactionBase.this.isCommitted();
            }

            @Override
            public boolean isReverted() {
                return TransactionBase.this.isReverted();
            }

            @Override
            public boolean isReadOnly() {
                return options.getReadOnly();
            }

        };

        public Transaction getTransaction() {
            return transaction;
        }

        protected TransactionBase(long id, AmbientThreadedTransactionScope suspended, TransactionOptions options) {
            this.id = id;
            this.suspended = suspended;

            this.options = options;
            if (this.options.getReadOnly()) {
                this.isRollbackOnly.set(true);
            }
        }

        protected void fireEvent(int event) {
            Set<TransactionListener> listeners = this.listeners.get();
            if (listeners == null)
                return;

            TransactionEventObject evtObj = new TransactionEventObject(ThreadContextTransactionManager.this,
                    this.transaction, event);
            synchronized (listeners) {
                for (Object l : listeners.toArray()) {
                    ((TransactionListener) l).TransactionEvent(evtObj);
                }
            }
        }

        @Override
        public boolean isCommitted() {
            return this.isCompleted.get() && commitId > -1;
        }

        @Override
        public boolean isReverted() {
            return isCompleted.get() && commitId == -1;
        }

        @Override
        public void putTransactionListener(TransactionListener listener) {
            Set<TransactionListener> l = this.listeners.get();
            if (l == null) {
                l = new HashSet<>();
                if (!this.listeners.compareAndSet(null, l)) {
                    l = this.listeners.get();
                }
            }
            synchronized (l) {
                l.add(listener);
            }
        }

        @Override
        public void removeTransactionListener(TransactionListener listener) {
            Set<TransactionListener> l = this.listeners.get();
            if (l == null) {
                return;
            }
            synchronized (l) {
                l.remove(listener);
            }
        }

        @Override
        public void setRevertOnly() {
            this.isRollbackOnly.set(true);
        }

        @Override
        public TransactionOptions getOptions() {
            return this.options;
        }

        @Override
        public void close() {
            if (suspended != null && !suspended.isClosed) {
                //need to put back the suspended transaction
                ThreadContextTransactionManager.this.currentTransactions.put(getContextId(), suspended);
                suspended = null;
            }

            this.isClosed = true;
        }
    }

    /**
     * A Transaction that is meant to exist within the context of one thread.
     * There should be no cross-thread transactional data access, only cross-thread
     * state querying.
     */
    protected class AmbientThreadedTransactionScope extends TransactionBase {

        private List<WrappingTransactionScope> uncommittedScopes = new LinkedList<>();
        private List<WrappingTransactionScope> wrappingScopes = new LinkedList<>();

        protected AmbientThreadedTransactionScope(long id, AmbientThreadedTransactionScope suspended,
                TransactionOptions options) {
            super(id, suspended, options);
        }

        @Override
        public void commit() throws TransactionException {
            throw new UnsupportedOperationException("should not be called directly");
        }

        private void doCommit() throws TransactionException {
            if (this.isRollbackOnly.get()) {
                throw new IllegalTransactionStateException("Cannot commit a rollback-only transaction");
            }
            if (this.isCompleted.get()) {
                throw new IllegalTransactionStateException("Cannot commit a completed transaction");
            }

            commitId = generateNewId();
            ThreadContextTransactionManager.this.commit(this);

            //soon as commit returns, we are committed.
            this.isCompleted.set(true);

            fireEvent(TransactionEventObject.COMMITTED);
        }

        void completeWrappingScope(WrappingTransactionScope scope) throws TransactionException {
            if (uncommittedScopes.remove(scope) && uncommittedScopes.isEmpty()) {
                //all wrapping transaction scopes have completed, we can commit
                doCommit();
            }
            //otherwise we do nothing, simply mark the wrapping scope as completed by removing it from the list.
        }

        void addWrappingScope(WrappingTransactionScope scope) {
            synchronized (this) {
                //add them at the beginning because the most recent ones
                //are most likely to close first.
                if (!scope.getOptions().getReadOnly()) {
                    //only add it to uncommitted scopes if it can actually write.
                    //ReadOnly scopes can close without commit, but an explicit revert
                    //will still revert the entire ambient scope.
                    uncommittedScopes.add(0, scope);
                }
                wrappingScopes.add(0, scope);
            }
        }

        @Override
        public void revert() {
            if (!isCompleted.compareAndSet(false, true)) {
                throw new IllegalTransactionStateException("Cannot rollback a completed transaction");
            }

            if (!this.options.getReadOnly()) {
                doRevert();
            }

            fireEvent(TransactionEventObject.REVERTED);
        }

        protected void doRevert() {
            ThreadContextTransactionManager.this.revert(this.boundEngines, this.id, false);
        }

        void closeWrappingScope(WrappingTransactionScope scope) {
            synchronized (this) {
                if (uncommittedScopes.remove(scope) && !this.isCompleted.get()) {
                    //the scope was uncommitted, need to revert the transaction
                    revert();
                }

                if (wrappingScopes.remove(scope) && wrappingScopes.isEmpty()) {
                    //all wrapping transaction scopes have closed, we can close.
                    close();
                }
                //otherwise, can't close yet, still have some open scopes.
            }
        }

        @Override
        public void close() {
            if (isCompleted.compareAndSet(false, true)) {
                //we completed in the close, need to revert.
                doRevert();
            }

            //remove the transaction scope from the current transactions map
            Iterator<AmbientThreadedTransactionScope> it = currentTransactions.values().iterator();
            while (it.hasNext()) {
                //Object equality because we don't know which 
                if (it.next() == this) {
                    it.remove();
                    break;
                }
            }

            super.close();
        }
    }

    /** 
     * A transaction that implements the {@link Propagation#NOT_SUPPORTED} behavior,
     * maintaining a reference to the suspended transaction so that it can be
     * replaced when this is closed.
     */
    protected class NotSupportedTransaction extends TransactionBase {
        public NotSupportedTransaction(AmbientThreadedTransactionScope suspended, TransactionOptions options) {
            super(-1, suspended, options);
        }

        @Override
        public void commit() throws TransactionException {
            throw new IllegalTransactionStateException(
                    "Cannot commit a transaction opened with propagation " + "NEVER or NOT_SUPPORTED");
        }

        @Override
        public void revert() {
            throw new IllegalTransactionStateException(
                    "Cannot revert a transaction opened with propagation " + "NEVER or NOT_SUPPORTED");
        }

    }

    /**
     * A transaction object that represents no open transaction.  This is
     * created by opening a transaction with the {@link Propagation#NEVER} or
     * with {@link Propagation#NOT_SUPPORTED} when the 
     */
    protected class EmptyTransactionScope implements TransactionScope {

        private TransactionOptions options;

        private volatile boolean isCommitted = false;
        private volatile boolean isReverted = false;
        private volatile boolean isClosed = false;

        public EmptyTransactionScope(TransactionOptions options) {
            this.options = options;
        }

        @Override
        public void commit() throws TransactionException {
            throw new IllegalTransactionStateException(
                    "Cannot commit a transaction opened with propagation " + "NEVER or NOT_SUPPORTED");
        }

        @Override
        public void revert() {
            throw new IllegalTransactionStateException(
                    "Cannot revert a transaction opened with propagation " + "NEVER or NOT_SUPPORTED");
        }

        @Override
        public void setRevertOnly() {

        }

        @Override
        public boolean isCommitted() {
            return isCommitted;
        }

        @Override
        public boolean isReverted() {
            return isReverted;
        }

        @Override
        public TransactionOptions getOptions() {
            return options;
        }

        @Override
        public void close() {
            //nothing to do
            isClosed = true;
        }

        @Override
        public void putTransactionListener(TransactionListener listener) {

        }

        @Override
        public void removeTransactionListener(TransactionListener listener) {

        }

    }

    /**
     * A TransactionScope object that provides a view onto the ambient scope.
     * There may be multiple wrapping transaction scopes all pointing to the
     * same ambient transaction scope.  When ALL of these is committed, the
     * underlying ambient transaction is committed.
     */
    protected class WrappingTransactionScope implements TransactionScope {

        private AmbientThreadedTransactionScope wrapped;

        private TransactionOptions options;

        protected WrappingTransactionScope(AmbientThreadedTransactionScope wrapped, boolean isReadOnly) {
            this.wrapped = wrapped;
            this.options = wrapped.getOptions().withReadOnly(isReadOnly);

            wrapped.addWrappingScope(this);
        }

        @Override
        public void commit() throws TransactionException {
            wrapped.completeWrappingScope(this);
        }

        @Override
        public void revert() {
            wrapped.revert();
        }

        @Override
        public void setRevertOnly() {
            wrapped.setRevertOnly();
        }

        @Override
        public boolean isCommitted() {
            return wrapped.isCommitted();
        }

        @Override
        public boolean isReverted() {
            return wrapped.isReverted();
        }

        @Override
        public TransactionOptions getOptions() {
            return this.options;
        }

        @Override
        public void close() {
            wrapped.closeWrappingScope(this);
        }

        @Override
        public void putTransactionListener(TransactionListener listener) {
            wrapped.putTransactionListener(listener);
        }

        @Override
        public void removeTransactionListener(TransactionListener listener) {
            wrapped.removeTransactionListener(listener);
        }

    }

    //<editor-fold desc="transaction journal">
    private class TransactionJournalEntry {
        public long txId;
        public long commitId;

        public Set<String> tableNames = new HashSet<>();
    }

    private Converter<TransactionJournalEntry, Element> toElement = new Converter<TransactionJournalEntry, Element>() {
        @Override
        public Element convert(TransactionJournalEntry source) throws ConversionException {
            Element ret = new Element("entry");
            ret.setAttribute("txId", Long.toString(source.txId));
            ret.setAttribute("commit", Long.toString(source.commitId));

            for (String s : source.tableNames) {
                ret.addContent(new Element("table").setText(s));
            }

            return ret;
        }
    };

    private Converter<Element, TransactionJournalEntry> fromElement = new Converter<Element, TransactionJournalEntry>() {
        @Override
        public TransactionJournalEntry convert(Element source) throws ConversionException {
            TransactionJournalEntry ret = new TransactionJournalEntry();

            try {
                String txId = source.getAttributeValue("txId");
                if (txId == null) {
                    throw new ConversionException("txId attribute required");
                }
                ret.txId = Long.parseLong(txId);

                String commitId = source.getAttributeValue("commit");
                if (commitId != null) {
                    ret.commitId = Long.parseLong(commitId);
                }

                for (Element e : source.getChildren("table")) {
                    ret.tableNames.add(e.getText());
                }
            } catch (NumberFormatException ex) {
                throw new ConversionException("Conversion failure", ex);
            }

            return ret;
        }
    };

    //</editor-fold>
}