org.cinchapi.concourse.server.ConcourseServer.java Source code

Java tutorial

Introduction

Here is the source code for org.cinchapi.concourse.server.ConcourseServer.java

Source

/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2013-2014 Jeff Nelson, Cinchapi Software Collective
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.cinchapi.concourse.server;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executors;

import javax.annotation.Nullable;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;

import org.apache.thrift.TException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.server.TThreadPoolServer.Args;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;
import org.cinchapi.concourse.annotate.Atomic;
import org.cinchapi.concourse.lang.ConjunctionSymbol;
import org.cinchapi.concourse.lang.Expression;
import org.cinchapi.concourse.lang.Parser;
import org.cinchapi.concourse.lang.PostfixNotationSymbol;
import org.cinchapi.concourse.lang.Symbol;
import org.cinchapi.concourse.lang.Translate;
import org.cinchapi.concourse.security.AccessManager;
import org.cinchapi.concourse.server.io.FileSystem;
import org.cinchapi.concourse.server.jmx.ConcourseServerMXBean;
import org.cinchapi.concourse.server.jmx.ManagedOperation;
import org.cinchapi.concourse.server.storage.AtomicOperation;
import org.cinchapi.concourse.server.storage.AtomicStateException;
import org.cinchapi.concourse.server.storage.BufferedStore;
import org.cinchapi.concourse.server.storage.Compoundable;
import org.cinchapi.concourse.server.storage.Engine;
import org.cinchapi.concourse.server.storage.Transaction;
import org.cinchapi.concourse.server.storage.TransactionStateException;
import org.cinchapi.concourse.shell.CommandLine;
import org.cinchapi.concourse.thrift.AccessToken;
import org.cinchapi.concourse.thrift.ConcourseService;
import org.cinchapi.concourse.thrift.TCriteria;
import org.cinchapi.concourse.thrift.TObject;
import org.cinchapi.concourse.thrift.ConcourseService.Iface;
import org.cinchapi.concourse.thrift.Operator;
import org.cinchapi.concourse.thrift.TSecurityException;
import org.cinchapi.concourse.thrift.TSymbol;
import org.cinchapi.concourse.thrift.TTransactionException;
import org.cinchapi.concourse.thrift.TransactionToken;
import org.cinchapi.concourse.time.Time;
import org.cinchapi.concourse.util.Convert;
import org.cinchapi.concourse.util.Convert.ResolvableLink;
import org.cinchapi.concourse.util.Environments;
import org.cinchapi.concourse.util.Logger;
import org.cinchapi.concourse.util.TLinkedHashMap;
import org.cinchapi.concourse.util.TSets;
import org.cinchapi.concourse.util.Version;
import org.cinchapi.concourse.Link;
import org.cinchapi.concourse.thrift.Type;
import org.cliffc.high_scale_lib.NonBlockingHashMap;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import static org.cinchapi.concourse.server.GlobalState.*;

/**
 * Accepts requests from clients to read and write data in Concourse. The server
 * is configured with a {@code concourse.prefs} file.
 * 
 * @author jnelson
 */
public class ConcourseServer implements ConcourseService.Iface, ConcourseServerMXBean {

    /**
     * Run the server...
     * 
     * @param args
     * @throws TTransportException
     * @throws MalformedObjectNameException
     * @throws NotCompliantMBeanException
     * @throws MBeanRegistrationException
     * @throws InstanceAlreadyExistsException
     */
    public static void main(String... args) throws TTransportException, MalformedObjectNameException,
            InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
        final ConcourseServer server = new ConcourseServer();

        // Ensure the application is properly configured
        MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
        if (heap.getInit() < MIN_HEAP_SIZE) {
            System.err.println(
                    "Cannot initialize Concourse Server with " + "a heap smaller than " + MIN_HEAP_SIZE + " bytes");
            System.exit(127);
        }

        // Register MXBean
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("org.cinchapi.concourse.server.jmx:type=ConcourseServerMXBean");
        mbs.registerMBean(server, name);

        // Start the server...
        Thread serverThread = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    CommandLine.displayWelcomeBanner();
                    server.start();
                } catch (TTransportException e) {
                    e.printStackTrace();
                    System.exit(-1);
                }
            }

        }, "main");
        serverThread.start();

        // Prepare for graceful shutdown...
        // NOTE: It may be necessary to run the Java VM with
        // -Djava.net.preferIPv4Stack=true
        final Thread shutdownThread = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    ServerSocket socket = new ServerSocket(SHUTDOWN_PORT);
                    socket.accept(); // block until a shutdown request is made
                    Logger.info("Shutdown request received");
                    server.stop();
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }

        }, "Shutdown");
        shutdownThread.setDaemon(true);
        shutdownThread.start();

        // Add a shutdown hook that launches the official {@link ShutdownRunner}
        // in cases where the server process is directly killed (i.e. from the
        // tanuki scripts)
        Runtime.getRuntime().addShutdownHook(new Thread() {

            @Override
            public void run() {
                ShutdownRunner.main();
                try {
                    shutdownThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });
    }

    /**
     * Contains the credentials used by the {@link #manager}. This file is
     * typically located in the root of the server installation.
     */
    private static final String ACCESS_FILE = ".access";

    /**
     * The Thrift server controls the RPC protocol. Use
     * https://github.com/m1ch1/mapkeeper/wiki/Thrift-Java-Servers-Compared for
     * a reference.
     */
    private final TServer server;

    /**
     * A mapping from env to the corresponding Engine that controls all
     * the logic for data storage and retrieval.
     */
    private final Map<String, Engine> engines;

    /**
     * The base location where the indexed database records are stored.
     */
    private final String dbStore;

    /**
     * The base location where the indexed buffer pages are stored.
     */
    private final String bufferStore;

    /**
     * The AccessManager controls access to the server.
     */
    private final AccessManager manager;

    private static final int MIN_HEAP_SIZE = 268435456; // 256 MB

    private static final int NUM_WORKER_THREADS = 100; // This may become
                                                       // configurable in a
                                                       // prefs file in a
                                                       // future release.
    /**
     * The server maintains a collection of {@link Transaction} objects to
     * ensure that client requests are properly routed. When the client makes a
     * call to {@link #stage(AccessToken)}, a Transaction is started on the
     * server and a {@link TransactionToken} is used for the client to reference
     * that Transaction in future calls.
     */
    private final Map<TransactionToken, Transaction> transactions = new NonBlockingHashMap<TransactionToken, Transaction>();

    /**
     * Construct a ConcourseServer that listens on {@link #SERVER_PORT} and
     * stores data in {@link Properties#DATA_HOME}.
     * 
     * @throws TTransportException
     */
    public ConcourseServer() throws TTransportException {
        this(CLIENT_PORT, BUFFER_DIRECTORY, DATABASE_DIRECTORY);
    }

    /**
     * Construct a ConcourseServer that listens on {@code port} and store data
     * in {@code dbStore} and {@code bufferStore}.
     * 
     * @param port
     * @param bufferStore
     * @param dbStore
     * @throws TTransportException
     */
    public ConcourseServer(int port, String bufferStore, String dbStore) throws TTransportException {
        Preconditions.checkState(!bufferStore.equalsIgnoreCase(dbStore),
                "Cannot store buffer and database files in the same directory. " + "Please check concourse.prefs.");
        Preconditions.checkState(!Strings.isNullOrEmpty(Environments.sanitize(DEFAULT_ENVIRONMENT)),
                "Cannot initialize " + "Concourse Server with a default environment of "
                        + "'%s'. Please use a default environment name that "
                        + "contains only alphanumeric characters.",
                DEFAULT_ENVIRONMENT);
        FileSystem.mkdirs(bufferStore);
        FileSystem.mkdirs(dbStore);
        TServerSocket socket = new TServerSocket(port);
        ConcourseService.Processor<Iface> processor = new ConcourseService.Processor<Iface>(this);
        Args args = new TThreadPoolServer.Args(socket);
        args.processor(processor);
        args.maxWorkerThreads(NUM_WORKER_THREADS);
        args.executorService(
                Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("Server" + "-%d").build()));
        this.server = new TThreadPoolServer(args);
        this.bufferStore = bufferStore;
        this.dbStore = dbStore;
        this.engines = Maps.newConcurrentMap();
        this.manager = AccessManager.create(ACCESS_FILE);
        getEngine(); // load the default engine
    }

    @Override
    public void abort(AccessToken creds, TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        transactions.remove(transaction).abort();
    }

    @Override
    public boolean add(String key, TObject value, long record, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            if (value.getType() != Type.LINK || isValidLink((Link) Convert.thriftToJava(value), record)) {
                return ((BufferedStore) getStore(transaction, env)).add(key, value, record);
            } else {
                return false;
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public Map<Long, String> audit(long record, String key, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return Strings.isNullOrEmpty(key) ? getStore(transaction, env).audit(record)
                    : getStore(transaction, env).audit(key, record);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }

    }

    @Override
    public Map<String, Set<TObject>> browse0(long record, long timestamp, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return timestamp == 0 ? getStore(transaction, env).browse(record)
                    : getStore(transaction, env).browse(record, timestamp);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public Map<TObject, Set<Long>> browse1(String key, long timestamp, AccessToken creds,
            TransactionToken transaction, String env) throws TSecurityException, TException {
        checkAccess(creds, transaction);
        try {
            return timestamp == 0 ? getStore(transaction, env).browse(key)
                    : getStore(transaction, env).browse(key, timestamp);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    @Atomic
    public Map<Long, Set<TObject>> chronologize(long record, String key, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            Compoundable store = getStore(transaction, env);
            Map<Long, Set<TObject>> result = TLinkedHashMap.newTLinkedHashMap();
            Map<Long, String> history = store.audit(key, record);
            for (Long timestamp : history.keySet()) {
                Set<TObject> values = store.fetch(key, record, timestamp);
                if (!values.isEmpty()) {
                    result.put(timestamp, values);
                }
            }
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk) || (operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = updateChronologizeResultSet(key, record, result, history, store);
            }
            return result;
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Atomic
    @Override
    public void clear(String key, long record, AccessToken creds, TransactionToken transaction, String env)
            throws TException {
        checkAccess(creds, transaction);
        try {
            Compoundable store = getStore(transaction, env);
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk) || (operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = doClear(key, record, store);
            }
            if (operation == null) {
                throw new TTransactionException();
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Atomic
    @Override
    public void clear1(long record, AccessToken creds, TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            Compoundable store = getStore(transaction, env);
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk) || (operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = doClear(record, store);
            }
            if (operation == null) {
                throw new TTransactionException();
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public boolean commit(AccessToken creds, TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return transactions.remove(transaction).commit();
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public Set<String> describe(long record, long timestamp, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return timestamp == 0 ? getStore(transaction, env).describe(record)
                    : getStore(transaction, env).describe(record, timestamp);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @ManagedOperation
    @Override
    @Deprecated
    public String dump(String id) {
        return dump(DEFAULT_ENVIRONMENT);
    }

    @Override
    public String dump(String id, String env) {
        return getEngine(env).dump(id);
    }

    @Override
    public Set<TObject> fetch(String key, long record, long timestamp, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return timestamp == 0 ? getStore(transaction, env).fetch(key, record)
                    : getStore(transaction, env).fetch(key, record, timestamp);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public Set<Long> find(String key, Operator operator, List<TObject> values, long timestamp, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            TObject[] tValues = values.toArray(new TObject[values.size()]);
            return timestamp == 0 ? getStore(transaction, env).find(key, operator, tValues)
                    : getStore(transaction, env).find(timestamp, key, operator, tValues);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Atomic
    @Override
    public Set<Long> find1(TCriteria tcriteria, AccessToken creds, TransactionToken transaction, String env)
            throws TSecurityException, TException {
        checkAccess(creds, transaction);
        try {
            List<Symbol> symbols = Lists.newArrayList();
            for (TSymbol tsymbol : tcriteria.getSymbols()) {
                symbols.add(Translate.fromThrift(tsymbol));
            }
            Queue<PostfixNotationSymbol> queue = Parser.toPostfixNotation(symbols);
            Deque<Set<Long>> stack = new ArrayDeque<Set<Long>>();
            Compoundable store = getStore(transaction, env);
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk) || (operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = doFind1(queue, stack, store);
            }
            return Sets.newTreeSet(stack.pop());
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    @ManagedOperation
    @Deprecated
    public String getDumpList() {
        return getDumpList(DEFAULT_ENVIRONMENT);
    }

    @Override
    public String getDumpList(String env) {
        return getEngine(env).getDumpList();
    }

    @Override
    public String getServerEnvironment(AccessToken creds, TransactionToken transaction, String env)
            throws TSecurityException, TException {
        checkAccess(creds, transaction);
        return Environments.sanitize(env);
    }

    @Override
    @ManagedOperation
    public String getServerVersion() {
        return Version.getVersion(ConcourseServer.class).toString();
    }

    @Override
    @ManagedOperation
    public void grant(byte[] username, byte[] password) {
        manager.grant(ByteBuffer.wrap(username), ByteBuffer.wrap(password));
        username = null;
        password = null;
    }

    @Override
    @ManagedOperation
    public boolean hasUser(byte[] username) {
        return manager.isValidUsername(ByteBuffer.wrap(username));
    }

    @Atomic
    @Override
    public boolean insert(String json, long record, AccessToken creds, TransactionToken transaction, String env)
            throws TException {
        checkAccess(creds, transaction);
        AtomicOperation operation = getStore(transaction, env).startAtomicOperation();
        try {
            Multimap<String, Object> data = Convert.jsonToJava(json);
            for (String key : data.keySet()) {
                for (Object value : data.get(key)) {
                    if (value instanceof ResolvableLink) {
                        ResolvableLink rl = (ResolvableLink) value;
                        Set<Long> links = operation.find(rl.getKey(), Operator.EQUALS,
                                Convert.javaToThrift(rl.getValue()));
                        for (long link : links) {
                            TObject t = Convert.javaToThrift(Link.to(link));
                            if (!operation.add(key, t, record)) {
                                return false;
                            }
                        }
                    } else if (!operation.add(key, Convert.javaToThrift(value), record)) {
                        return false;
                    }
                }
            }
            return operation.commit();
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        } catch (AtomicStateException e) {
            return false;
        }
    }

    @Atomic
    @Override
    public long insert1(String json, AccessToken creds, TransactionToken transaction, String env)
            throws TSecurityException, TException {
        long record = 0;
        checkAccess(creds, transaction);
        try {
            AtomicOperation operation = null;
            while (operation == null || operation != null && !operation.commit()) {
                record = Time.now();
                operation = insertIntoEmptyRecord(json, record, getStore(transaction, env));
            }
            return record;
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    @ManagedOperation
    public boolean login(byte[] username, byte[] password) {
        // NOTE: Any existing sessions for the user will be invalidated.
        try {
            AccessToken token = login(ByteBuffer.wrap(username), ByteBuffer.wrap(password), null); // TODO get real
                                                                                                   // env
            username = null;
            password = null;
            if (token != null) {
                logout(token, null); // TODO get real env
                return true;
            } else {
                return false;
            }
        } catch (TSecurityException e) {
            return false;
        } catch (TException e) {
            throw Throwables.propagate(e);
        }
    }

    @Override
    public AccessToken login(ByteBuffer username, ByteBuffer password, String env) throws TException {
        validate(username, password);
        getEngine(env);
        return manager.authorize(username);
    }

    @Override
    public void logout(AccessToken creds, String env) throws TException {
        checkAccess(creds, null);
        manager.deauthorize(creds);
    }

    @Override
    public boolean ping(long record, AccessToken creds, TransactionToken transaction, String env)
            throws TException {
        checkAccess(creds, transaction);
        try {
            return !getStore(transaction, env).describe(record).isEmpty();
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public boolean remove(String key, TObject value, long record, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            if (value.getType() != Type.LINK || isValidLink((Link) Convert.thriftToJava(value), record)) {
                return ((BufferedStore) getStore(transaction, env)).remove(key, value, record);
            } else {
                return false;
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Atomic
    @Override
    public void revert(String key, long record, long timestamp, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            Compoundable store = getStore(transaction, env);
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk)
                    || (operation != null && operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = doRevert(key, record, timestamp, store);
            }
            if (operation == null) {
                throw new TTransactionException();
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    @ManagedOperation
    public void revoke(byte[] username) {
        manager.revoke(ByteBuffer.wrap(username));
        username = null;
    }

    @Override
    public Set<Long> search(String key, String query, AccessToken creds, TransactionToken transaction, String env)
            throws TException {
        checkAccess(creds, transaction);
        try {
            return getStore(transaction, env).search(key, query);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public void set0(String key, TObject value, long record, AccessToken creds, TransactionToken transaction,
            String env) throws TException {
        checkAccess(creds, transaction);
        try {
            Compoundable store = getStore(transaction, env);
            boolean nullOk = true;
            boolean retryable = store instanceof Engine;
            AtomicOperation operation = null;
            while ((operation == null && nullOk)
                    || (operation != null && operation != null && !operation.commit() && retryable)) {
                nullOk = false;
                operation = doSet(key, value, record, store);
            }
            if (operation == null) {
                throw new TTransactionException();
            }
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Override
    public TransactionToken stage(AccessToken creds, String env) throws TException {
        checkAccess(creds, null);
        TransactionToken token = new TransactionToken(creds, Time.now());
        Transaction transaction = getEngine(env).startTransaction();
        transactions.put(token, transaction);
        Logger.info("Started Transaction {}", transaction);
        return token;
    }

    /**
     * Start the server.
     * 
     * @throws TTransportException
     */
    public void start() throws TTransportException {
        for (Engine engine : engines.values()) {
            engine.start();
        }
        System.out.println("The Concourse server has started");
        server.serve();
    }

    /**
     * Stop the server.
     */
    public void stop() {
        if (server.isServing()) {
            server.stop();
            for (Engine engine : engines.values()) {
                engine.stop();
            }
            System.out.println("The Concourse server has stopped");
        }
    }

    @Override
    public boolean verify(String key, TObject value, long record, long timestamp, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        try {
            return timestamp == 0 ? getStore(transaction, env).verify(key, value, record)
                    : getStore(transaction, env).verify(key, value, record, timestamp);
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        }
    }

    @Atomic
    @Override
    public boolean verifyAndSwap(String key, TObject expected, long record, TObject replacement, AccessToken creds,
            TransactionToken transaction, String env) throws TException {
        checkAccess(creds, transaction);
        AtomicOperation operation = getStore(transaction, env).startAtomicOperation();
        try {
            return (operation.verify(key, expected, record) && operation.remove(key, expected, record)
                    && operation.add(key, replacement, record)) ? operation.commit() : false;
        } catch (TransactionStateException e) {
            throw new TTransactionException();
        } catch (AtomicStateException e) {
            return false;
        }
    }

    /**
     * Check to make sure that {@code creds} and {@code transaction} are valid
     * and are associated with one another.
     * 
     * @param creds
     * @param transaction
     * @throws TSecurityException
     * @throws IllegalArgumentException
     */
    private void checkAccess(AccessToken creds, @Nullable TransactionToken transaction)
            throws TSecurityException, IllegalArgumentException {
        if (!manager.validate(creds)) {
            throw new TSecurityException("Invalid access token");
        }
        Preconditions.checkArgument((transaction != null && transaction.getAccessToken().equals(creds)
                && transactions.containsKey(transaction)) || transaction == null);
    }

    /**
     * Start an {@link AtomicOperation} with {@code store} as the destination
     * and do the work to clear {@code record}.
     * 
     * @param record
     * @param store
     * @return the AtomicOperation
     */
    private AtomicOperation doClear(long record, Compoundable store) {
        AtomicOperation operation = store.startAtomicOperation();
        try {
            Map<String, Set<TObject>> values = operation.browse(record);
            for (Map.Entry<String, Set<TObject>> entry : values.entrySet()) {
                String key = entry.getKey();
                Set<TObject> valueSet = entry.getValue();
                for (TObject value : valueSet) {
                    operation.remove(key, value, record);
                }
            }
            return operation;
        } catch (AtomicStateException e) {
            return null;
        }
    }

    /**
     * Start an {@link AtomicOperation} with {@code store} as the destination
     * and do the work to clear {@code key} in {@code record}.
     * 
     * @param key
     * @param record
     * @param store
     * @return the AtomicOperation
     */
    private AtomicOperation doClear(String key, long record, Compoundable store) {
        AtomicOperation operation = store.startAtomicOperation();
        try {
            Set<TObject> values = operation.fetch(key, record);
            for (TObject value : values) {
                operation.remove(key, value, record);
            }
            return operation;
        } catch (AtomicStateException e) {
            return null;
        }
    }

    /**
     * Do the work necessary to complete the
     * {@link #find1(TCriteria, long, AccessToken, TransactionToken)} method as
     * an AtomicOperation.
     * 
     * @param queue
     * @param stack
     * @param store
     * @return the AtomicOperation
     */
    private AtomicOperation doFind1(Queue<PostfixNotationSymbol> queue, Deque<Set<Long>> stack,
            Compoundable store) {
        // TODO there is room to do some query planning/optimization by going
        // through the pfn and plotting an Abstract Syntax Tree and looking for
        // the optimal routes to start with
        Preconditions.checkArgument(stack.isEmpty());
        AtomicOperation operation = store.startAtomicOperation();
        for (PostfixNotationSymbol symbol : queue) {
            if (symbol == ConjunctionSymbol.AND) {
                stack.push(TSets.intersection(stack.pop(), stack.pop()));
            } else if (symbol == ConjunctionSymbol.OR) {
                stack.push(TSets.union(stack.pop(), stack.pop()));
            } else if (symbol instanceof Expression) {
                Expression exp = (Expression) symbol;
                stack.push(exp.getTimestampRaw() == 0
                        ? operation.find(exp.getKeyRaw(), exp.getOperatorRaw(), exp.getValuesRaw())
                        : operation.find(exp.getTimestampRaw(), exp.getKeyRaw(), exp.getOperatorRaw(),
                                exp.getValuesRaw()));
            } else {
                // If we reach here, then the conversion to postfix notation
                // failed :-/
                throw new IllegalStateException();
            }
        }
        return operation;
    }

    /**
     * Start an {@link AtomicOperation} with {@code store} as the destination
     * and do the work to revert {@code key} in {@code record} to
     * {@code timestamp}.
     * 
     * @param key
     * @param record
     * @param timestamp
     * @param store
     * @return the AtomicOperation that must be committed
     */
    private AtomicOperation doRevert(String key, long record, long timestamp, Compoundable store) {
        AtomicOperation operation = store.startAtomicOperation();
        try {
            Set<TObject> past = operation.fetch(key, record, timestamp);
            Set<TObject> present = operation.fetch(key, record);
            Set<TObject> xor = Sets.symmetricDifference(past, present);
            for (TObject value : xor) {
                if (present.contains(value)) {
                    operation.remove(key, value, record);
                } else {
                    operation.add(key, value, record);
                }
            }
            return operation;
        } catch (AtomicStateException e) {
            return null;
        }
    }

    /**
     * Start an {@link AtomicOperation} with {@code store} as the destination
     * and do the work to set {@code key} as {@code value} in {@code record}.
     * 
     * @param key
     * @param value
     * @param record
     * @param store
     * @return
     */
    private AtomicOperation doSet(String key, TObject value, long record, Compoundable store) {
        // NOTE: We cannot use the #clear() method because our removes must be
        // defined in terms of the AtomicOperation for true atomic safety.
        AtomicOperation operation = store.startAtomicOperation();
        try {
            Set<TObject> values = operation.fetch(key, record);
            for (TObject oldValue : values) {
                operation.remove(key, oldValue, record);
            }
            operation.add(key, value, record);
            return operation;
        } catch (AtomicStateException e) {
            return null;
        }

    }

    /**
     * Return the {@link Engine} that is associated with the
     * {@link Default#ENVIRONMENT}.
     * 
     * @return the Engine
     */
    private Engine getEngine() {
        return getEngine(DEFAULT_ENVIRONMENT);
    }

    /**
     * Return the {@link Engine} that is associated with {@code env}. If
     * such an Engine does not exist, create a new one and add it to the
     * collection.
     * 
     * @param env
     * @return the Engine
     */
    private Engine getEngine(String env) {
        env = Environments.sanitize(env);
        Engine engine = engines.get(env);
        if (engine == null) {
            engine = new Engine(bufferStore + File.separator + env, dbStore + File.separator + env, env);
            engine.start();
            engines.put(env, engine);
        }
        return engine;
    }

    /**
     * Return the correct store to use for a read/write operation depending upon
     * the {@code env} whether the client has submitted a {@code transaction}
     * token.
     * 
     * @param transaction
     * @param env
     * @return the store to use
     */
    private Compoundable getStore(TransactionToken transaction, String env) {
        return transaction != null ? transactions.get(transaction) : getEngine(env);
    }

    /**
     * Atomically insert all the data in the {@code json} string in
     * {@code record} as long as {@code record} is currently empty.
     * 
     * @param json
     * @param record
     * @param store
     * @return the AtomicOperation
     */
    private AtomicOperation insertIntoEmptyRecord(String json, long record, Compoundable store) {
        AtomicOperation operation = store.startAtomicOperation();
        if (operation.describe(record).isEmpty()) {
            Multimap<String, Object> data = Convert.jsonToJava(json);
            for (String key : data.keySet()) {
                for (Object value : data.get(key)) {
                    if (value instanceof ResolvableLink) {
                        ResolvableLink rl = (ResolvableLink) value;
                        Set<Long> links = operation.find(rl.getKey(), Operator.EQUALS,
                                Convert.javaToThrift(rl.getValue()));
                        for (long link : links) {
                            TObject t = Convert.javaToThrift(Link.to(link));
                            operation.add(key, t, record);
                        }
                    } else {
                        operation.add(key, Convert.javaToThrift(value), record);
                    }

                }
            }
            return operation;
        } else {
            return null;
        }
    }

    /**
     * Return {@code true} if adding {@code link} to {@code record} is valid.
     * 
     * @param link
     * @param record
     * @return {@code true} if the link is valid
     */
    private boolean isValidLink(Link link, long record) {
        return link.longValue() != record;
    }

    /**
     * Start an {@link AtomicOperation} with {@code store} as the destination
     * and do the work to update chronologized values in {@code key} in
     * {@code record} with respect to {@code history} audit.
     * 
     * @param key
     * @param record
     * @param result
     * @param history
     * @param store
     * @return the AtomicOperation that must be committed
     */
    private AtomicOperation updateChronologizeResultSet(String key, long record, Map<Long, Set<TObject>> result,
            Map<Long, String> history, Compoundable store) {
        AtomicOperation operation = store.startAtomicOperation();
        try {
            Map<Long, String> newResult = operation.audit(key, record);
            if (newResult.size() > history.size()) {
                for (int i = history.size(); i < newResult.size(); i++) {
                    Long timestamp = Iterables.get((Iterable<Long>) newResult.keySet(), i);
                    Set<TObject> values = operation.fetch(key, record);
                    if (!values.isEmpty()) {
                        result.put(timestamp, operation.fetch(key, record));
                    }
                }
            }
            return operation;
        } catch (AtomicStateException e) {
            return null;
        }
    }

    /**
     * Validate that the {@code username} and {@code password} pair represent
     * correct credentials. If not, throw a TSecurityException.
     * 
     * @param username
     * @param password
     * @throws TSecurityException
     */
    private void validate(ByteBuffer username, ByteBuffer password) throws TSecurityException {
        if (!manager.validate(username, password)) {
            throw new TSecurityException("Invalid username/password combination.");
        }
    }

}