Java tutorial
/* * 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; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import org.cinchapi.concourse.annotate.CompoundOperation; import org.cinchapi.concourse.config.ConcourseConfiguration; import org.cinchapi.concourse.lang.BuildableState; import org.cinchapi.concourse.lang.Criteria; import org.cinchapi.concourse.lang.Translate; import org.cinchapi.concourse.security.ClientSecurity; import org.cinchapi.concourse.thrift.AccessToken; import org.cinchapi.concourse.thrift.ConcourseService; import org.cinchapi.concourse.thrift.Operator; import org.cinchapi.concourse.thrift.TObject; 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.TLinkedTableMap; import org.cinchapi.concourse.util.Timestamps; import org.cinchapi.concourse.util.Transformers; import org.cinchapi.concourse.util.TLinkedHashMap; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * <p> * Concourse is a schemaless and distributed version control database with * optimistic availability, serializable transactions and full-text search. * Concourse provides a more intuitive approach to data management that is easy * to deploy, access and scale with minimal tuning while also maintaining the * referential integrity and ACID characteristics of traditional database * systems. * </p> * <h2>Data Model</h2> * <p> * The Concourse data model is lightweight and flexible which enables it to * support any kind of data at very large scales. Concourse trades unnecessary * structural notions of schemas, tables and indexes for a more natural modeling * of data based solely on the following concepts: * </p> * <p> * <ul> * <li><strong>Record</strong> — A logical grouping of data about a single * person, place, or thing (i.e. an object). Each {@code record} is a collection * of key/value pairs that are together identified by a unique primary key. * <li><strong>Key</strong> — An attribute that maps to a set of * <em>one or more</em> distinct {@code values}. A {@code record} can have many * different {@code keys}, and the {@code keys} in one {@code record} do not * affect those in another {@code record}. * <li><strong>Value</strong> — A dynamically typed quantity that is * mapped from a {@code key} in a {@code record}. * </ul> * </p> * <h4>Data Types</h4> * <p> * Concourse natively stores most of the Java primitives: boolean, double, * float, integer, long, and string (UTF-8). Otherwise, the value of the * {@link #toString()} method for the Object is stored. * </p> * <h4>Links</h4> * <p> * Concourse supports linking a {@code key} in one {@code record} to another * {@code record}. Links are one-directional, but it is possible to add two * links that are the inverse of each other to simulate bi-directionality (i.e. * link "friend" in Record 1 to Record 2 and link "friend" in Record 2 to Record * 1). * </p> * <h2>Transactions</h2> * <p> * By default, Concourse conducts every operation in {@code autocommit} mode * where every change is immediately written. Concourse also supports the * ability to stage a group of operations in transactions that are atomic, * consistent, isolated, and durable using the {@link #stage()}, * {@link #commit()} and {@link #abort()} methods. * * </p> * * @author jnelson */ public abstract class Concourse implements AutoCloseable { /** * Create a new Client connection to the environment of the Concourse * Server described in {@code concourse_client.prefs} (or the default * environment and server if the prefs file does not exist) and return a * handler to facilitate database interaction. * * @return the database handler */ public static Concourse connect() { return new Client(); } /** * /** * Create a new Client connection to the specified {@code environment} of * the Concourse Server described in {@code concourse_client.prefs} (or * the default server if the prefs file does not exist) and return a * handler to facilitate database interaction. * * @param environment * @return */ public static Concourse connect(String environment) { return new Client(environment); } /** * Create a new Client connection to the default environment of the * specified Concourse Server and return a handler to facilitate * database interaction. * * @param host * @param port * @param username * @param password * @return the database handler */ public static Concourse connect(String host, int port, String username, String password) { return new Client(host, port, username, password); } /** * Create a new Client connection to the specified {@code environment} of * the specified Concourse Server and return a handler to facilitate * database interaction. * * @param host * @param port * @param username * @param password * @param environment * @return the database handler */ public static Concourse connect(String host, int port, String username, String password, String environment) { return new Client(host, port, username, password, environment); } /** * Discard any changes that are currently staged for commit. * <p> * After this function returns, Concourse will return to {@code autocommit} * mode and all subsequent changes will be committed immediately. * </p> */ public abstract void abort(); /** * Add {@code key} as {@code value} in each of the {@code records} if it is * not already contained. * * @param key * @param value * @param records * @return a mapping from each record to a boolean indicating if * {@code value} is added */ @CompoundOperation public abstract Map<Long, Boolean> add(String key, Object value, Collection<Long> records); /** * Add {@code key} as {@code value} to {@code record} if it is not already * contained. * * @param key * @param value * @param record * @return {@code true} if {@code value} is added */ public abstract <T> boolean add(String key, T value, long record); /** * Audit {@code record} and return a log of revisions. * * @param record * @return a mapping from timestamp to a description of a revision */ public abstract Map<Timestamp, String> audit(long record); /** * Audit {@code key} in {@code record} and return a log of revisions. * * @param key * @param record * @return a mapping from timestamp to a description of a revision */ public abstract Map<Timestamp, String> audit(String key, long record); /** * Browse the {@code records} and return a mapping from each record to all * the data that is contained as a mapping from key name to value set. * * @param records * @return a mapping of all the contained keys and their mapped values in * each record */ public abstract Map<Long, Map<String, Set<Object>>> browse(Collection<Long> records); /** * Browse the {@code records} at {@code timestamp} and return a mapping from * each record to all the data that was contained as a mapping from key name * to value set. * * @param records * @param timestamp * @return a mapping of all the contained keys and their mapped values in * each record */ public abstract Map<Long, Map<String, Set<Object>>> browse(Collection<Long> records, Timestamp timestamp); /** * Browse {@code record} and return all the data that is presently contained * as a mapping from key name to value set. * <p> * <em>This method is the atomic equivalent of calling * {@code fetch(describe(record), record)}</em> * </p> * * @param record * @return a mapping of all the presently contained keys and their mapped * values */ public abstract Map<String, Set<Object>> browse(long record); /** * Browse {@code record} at {@code timestamp} and return all the data that * was contained as a mapping from key name to value set. * <p> * <em>This method is the atomic equivalent of calling * {@code fetch(describe(record, timestamp), record, timestamp)}</em> * </p> * * @param record * @param timestamp * @return a mapping of all the contained keys and their mapped * values */ public abstract Map<String, Set<Object>> browse(long record, Timestamp timestamp); /** * Browse {@code key} and return all the data that is indexed as a mapping * from value to the set of records containing the value for {@code key}. * * @param key * @return a mapping of all the indexed values and their associated records. */ public abstract Map<Object, Set<Long>> browse(String key); /** * Browse {@code key} at {@code timestamp} and return all the data that was * indexed as a mapping from value to the set of records that contained the * value for {@code key} . * * @param key * @param timestamp * @return a mapping of all the indexed values and their associated records. */ public abstract Map<Object, Set<Long>> browse(String key, Timestamp timestamp); /** * Chronologize non-empty sets of values in {@code key} from {@code record} * and return a mapping from each timestamp to the non-empty set of values. * * @param key * @param record * @return a chronological mapping from each timestamp to the set of values * that were contained for the key in record */ public abstract Map<Timestamp, Set<Object>> chronologize(String key, long record); /** * Chronologize non-empty sets of values in {@code key} from {@code record} * from {@code start} timestamp inclusively to present and return a mapping * from each timestamp to the non-emtpy set of values. * * @param key * @param record * @param start * @return a chronological mapping from each timestamp to the set of values * that were contained for the key in record from specified start * timestamp to present */ @CompoundOperation public abstract Map<Timestamp, Set<Object>> chronologize(String key, long record, Timestamp start); /** * Chronologize non-empty sets of values in {@code key} from {@code record} * from {@code start} timestamp inclusively to {@code end} timestamp * exclusively and return a mapping from each timestamp to the non-empty * set of values. * * @param key * @param record * @param start * @param end * @return a chronological mapping from each timestamp to the set of values * that were contained for the key in record from specified start * timestamp to specified end timestamp */ @CompoundOperation public abstract Map<Timestamp, Set<Object>> chronologize(String key, long record, Timestamp start, Timestamp end); /** * Clear every {@code key} and contained value in each of * the {@code records} by removing every value for each {@code key} in each * record. * * @param records */ @CompoundOperation public abstract void clear(Collection<Long> records); /** * Clear each of the {@code keys} in each of the {@code records} by removing * every value for each key in each record. * * @param keys * @param records */ @CompoundOperation public abstract void clear(Collection<String> keys, Collection<Long> records); /** * Clear each of the {@code keys} in {@code record} by removing every value * for each key. * * @param keys * @param record */ @CompoundOperation public abstract void clear(Collection<String> keys, long record); /** * Atomically clear {@code record} by removing each contained * key and their values. * * @param record */ public abstract void clear(long record); /** * Clear {@code key} in each of the {@code records} by removing every value * for {@code key} in each record. * * @param key * @param records */ @CompoundOperation public abstract void clear(String key, Collection<Long> records); /** * Atomically clear {@code key} in {@code record} by removing each contained * value. * * @param record */ public abstract void clear(String key, long record); @Override public final void close() throws Exception { exit(); } /** * Attempt to permanently commit all the currently staged changes. This * function returns {@code true} if and only if all the changes can be * successfully applied. Otherwise, this function returns {@code false} and * all the changes are aborted. * <p> * After this function returns, Concourse will return to {@code autocommit} * mode and all subsequent changes will be written immediately. * </p> * * @return {@code true} if all staged changes are successfully committed */ public abstract boolean commit(); /** * Create a new Record and return its Primary Key. * * @return the Primary Key of the new Record */ public abstract long create(); /** * Describe each of the {@code records} and return a mapping from each * record to the keys that currently have at least one value. * * @param records * @return the populated keys in each record */ @CompoundOperation public abstract Map<Long, Set<String>> describe(Collection<Long> records); /** * Describe each of the {@code records} at {@code timestamp} and return a * mapping from each record to the keys that had at least one value. * * @param records * @param timestamp * @return the populated keys in each record at {@code timestamp} */ @CompoundOperation public abstract Map<Long, Set<String>> describe(Collection<Long> records, Timestamp timestamp); /** * Describe {@code record} and return the keys that currently have at least * one value. * * @param record * @return the populated keys in {@code record} */ public abstract Set<String> describe(long record); /** * Describe {@code record} at {@code timestamp} and return the keys that had * at least one value. * * @param record * @param timestamp * @return the populated keys in {@code record} at {@code timestamp} */ public abstract Set<String> describe(long record, Timestamp timestamp); /** * Close the Client connection. */ public abstract void exit(); /** * Fetch each of the {@code keys} from each of the {@code records} and * return a mapping from each record to a mapping from each key to the * contained values. * * @param keys * @param records * @return the contained values for each of the {@code keys} in each of the * {@code records} */ @CompoundOperation public abstract Map<Long, Map<String, Set<Object>>> fetch(Collection<String> keys, Collection<Long> records); /** * Fetch each of the {@code keys} from each of the {@code records} at * {@code timestamp} and return a mapping from each record to a mapping from * each key to the contained values. * * @param keys * @param records * @param timestamp * @return the contained values for each of the {@code keys} in each * of the {@code records} at {@code timestamp} */ @CompoundOperation public abstract Map<Long, Map<String, Set<Object>>> fetch(Collection<String> keys, Collection<Long> records, Timestamp timestamp); /** * Fetch each of the {@code keys} from {@code record} and return a mapping * from each key to the contained values. * * @param keys * @param record * @return the contained values for each of the {@code keys} in * {@code record} */ @CompoundOperation public abstract Map<String, Set<Object>> fetch(Collection<String> keys, long record); /** * Fetch each of the {@code keys} from {@code record} at {@code timestamp} * and return a mapping from each key to the contained values. * * @param keys * @param record * @param timestamp * @return the contained values for each of the {@code keys} in * {@code record} at {@code timestamp} */ @CompoundOperation public abstract Map<String, Set<Object>> fetch(Collection<String> keys, long record, Timestamp timestamp); /** * Fetch {@code key} from each of the {@code records} and return a mapping * from each record to contained values. * * @param key * @param records * @return the contained values for {@code key} in each {@code record} */ @CompoundOperation public abstract Map<Long, Set<Object>> fetch(String key, Collection<Long> records); /** * Fetch {@code key} from} each of the {@code records} at {@code timestamp} * and return a mapping from each record to the contained values. * * @param key * @param records * @param timestamp * @return the contained values for {@code key} in each of the * {@code records} at {@code timestamp} */ @CompoundOperation public abstract Map<Long, Set<Object>> fetch(String key, Collection<Long> records, Timestamp timestamp); /** * Fetch {@code key} from {@code record} and return all the contained * values. * * @param key * @param record * @return the contained values */ public abstract Set<Object> fetch(String key, long record); /** * Fetch {@code key} from {@code record} at {@code timestamp} and return the * set of values that were mapped. * * @param key * @param record * @param timestamp * @return the contained values */ public abstract Set<Object> fetch(String key, long record, Timestamp timestamp); /** * Find and return the set of records that satisfy the {@code criteria}. * This is analogous to the SELECT action in SQL. * * @param criteria * @return the records that match the {@code criteria} */ public abstract Set<Long> find(Criteria criteria); /** * Find and return the set of records that satisfy the {@code criteria}. * This is analogous to the SELECT action in SQL. * * @param criteria * @return the records that match the {@code criteria} */ public abstract Set<Long> find(Object criteria); // this method exists in // case the caller forgets // to called #build() on // the CriteriaBuilder /** * Find {@code key} {@code operator} {@code value} and return the set of * records that satisfy the criteria. This is analogous to the SELECT action * in SQL. * * @param key * @param operator * @param value * @return the records that match the criteria */ public abstract Set<Long> find(String key, Operator operator, Object value); /** * Find {@code key} {@code operator} {@code value} and {@code value2} and * return the set of records that satisfy the criteria. This is analogous to * the SELECT action in SQL. * * @param key * @param operator * @param value * @param value2 * @return the records that match the criteria */ public abstract Set<Long> find(String key, Operator operator, Object value, Object value2); /** * Find {@code key} {@code operator} {@code value} and {@code value2} at * {@code timestamp} and return the set of records that satisfy the * criteria. This is analogous to the SELECT action in SQL. * * @param key * @param operator * @param value * @param value2 * @param timestamp * @return the records that match the criteria */ public abstract Set<Long> find(String key, Operator operator, Object value, Object value2, Timestamp timestamp); /** * Find {@code key} {@code operator} {@code value} at {@code timestamp} and * return the set of records that satisfy the criteria. This is analogous to * the SELECT action in SQL. * * @param key * @param operator * @param value * @return the records that match the criteria */ public abstract Set<Long> find(String key, Operator operator, Object value, Timestamp timestamp); /** * Get each of the {@code keys} from each of the {@code records} and return * a mapping from each record to a mapping of each key to the first * contained value. * * @param keys * @param records * @return the first contained value for each of the {@code keys} in each of * the {@code records} */ @CompoundOperation public abstract Map<Long, Map<String, Object>> get(Collection<String> keys, Collection<Long> records); /** * Get each of the {@code keys} from each of the {@code records} at * {@code timestamp} and return a mapping from each record to a mapping of * each key to the first contained value. * * @param keys * @param records * @param timestamp * @return the first contained value for each of the {@code keys} in each of * the {@code records} at {@code timestamp} */ @CompoundOperation public abstract Map<Long, Map<String, Object>> get(Collection<String> keys, Collection<Long> records, Timestamp timestamp); /** * Get each of the {@code keys} from {@code record} and return a mapping * from each key to the first contained value. * * @param keys * @param record * @return the first contained value for each of the {@code keys} in * {@code record} */ @CompoundOperation public abstract Map<String, Object> get(Collection<String> keys, long record); /** * Get each of the {@code keys} from {@code record} at {@code timestamp} and * return a mapping from each key to the first contained value. * * @param keys * @param record * @param timestamp * @return the first contained value for each of the {@code keys} in * {@code record} at {@code timestamp} */ @CompoundOperation public abstract Map<String, Object> get(Collection<String> keys, long record, Timestamp timestamp); /** * Get {@code key} from each of the {@code records} and return a mapping * from each record to the first contained value. * * @param key * @param records * @return the first contained value for {@code key} in each of the * {@code records} */ @CompoundOperation public abstract Map<Long, Object> get(String key, Collection<Long> records); /** * Get {@code key} from each of the {@code records} at {@code timestamp} and * return a mapping from each record to the first contained value. * * @param key * @param records * @param timestamp * @return the first contained value for {@code key} in each of the * {@code records} at {@code timestamp} */ @CompoundOperation public abstract Map<Long, Object> get(String key, Collection<Long> records, Timestamp timestamp); /** * Get {@code key} from {@code record} and return the first contained value * or {@code null} if there is none. Compared to * {@link #fetch(String, long)}, this method is suited for cases when the * caller is certain that {@code key} in {@code record} maps to a single * value of type {@code T}. * * @param key * @param record * @return the first contained value */ public abstract <T> T get(String key, long record); /** * Get {@code key} from {@code record} at {@code timestamp} and return the * first contained value or {@code null} if there was none. Compared to * {@link #fetch(String, long, Timestamp)}, this method is suited for cases * when the caller is certain that {@code key} in {@code record} mapped to a * single value of type {@code T} at {@code timestamp}. * * @param key * @param record * @param timestamp * @return the first contained value */ public abstract <T> T get(String key, long record, Timestamp timestamp); /** * Return the environment of the server that is currently in use by this * client. * * @return the server environment */ public abstract String getServerEnvironment(); /** * Return the version of the server to which this client is currently * connected. * * @return the server version */ public abstract String getServerVersion(); /** * Atomically insert the key/value mappings described in the {@code json} * formatted string into a new record. * <p> * The {@code json} formatted string must describe an JSON object that * contains one or more keys, each of which maps to a JSON primitive or an * array of JSON primitives. * </p> * * @param json * @return the primary key of the new record or {@code null} if the insert * is unsuccessful */ public abstract long insert(String json); /** * Insert the key/value mappings described in the {@code json} formated * string into each of the {@code records}. * <p> * The {@code json} formatted string must describe an JSON object that * contains one or more keys, each of which maps to a JSON primitive or an * array of JSON primitives. * </p> * * @param json * @param records * @return a mapping from each primary key to a boolean describing if the * data was successfully inserted into that record */ @CompoundOperation public abstract Map<Long, Boolean> insert(String json, Collection<Long> records); /** * Atomically insert the key/value mappings described in the {@code json} * formatted string into {@code record}. * <p> * The {@code json} formatted string must describe an JSON object that * contains one or more keys, each of which maps to a JSON primitive or an * array of JSON primitives. * </p> * * @param json * @param record * @return {@code true} if the data is inserted into {@code record} */ public abstract boolean insert(String json, long record); /** * Link {@code key} in {@code source} to each of the {@code destinations}. * * @param key * @param source * @param destinations * @return a mapping from each destination to a boolean indicating if the * link was added */ public abstract Map<Long, Boolean> link(String key, long source, Collection<Long> destinations); /** * Link {@code key} in {@code source} to {@code destination}. * * @param key * @param source * @param destination * @return {@code true} if the link is added */ public abstract boolean link(String key, long source, long destination); /** * Ping each of the {@code records}. * * @param records * @return a mapping from each record to a boolean indicating if the record * currently has at least one populated key */ @CompoundOperation public abstract Map<Long, Boolean> ping(Collection<Long> records); /** * Ping {@code record}. * * @param record * @return {@code true} if {@code record} currently has at least one * populated key */ public abstract boolean ping(long record); /** * Remove {@code key} as {@code value} in each of the {@code records} if it * is contained. * * @param key * @param value * @param records * @return a mapping from each record to a boolean indicating if * {@code value} is removed */ @CompoundOperation public abstract Map<Long, Boolean> remove(String key, Object value, Collection<Long> records); /** * Remove {@code key} as {@code value} to {@code record} if it is contained. * * @param key * @param value * @param record * @return {@code true} if {@code value} is removed */ public abstract <T> boolean remove(String key, T value, long record); /** * Revert each of the {@code keys} in each of the {@code records} to * {@code timestamp} by creating new revisions that the relevant changes * that have occurred since {@code timestamp}. * * @param keys * @param records * @param timestamp */ @CompoundOperation public abstract void revert(Collection<String> keys, Collection<Long> records, Timestamp timestamp); /** * Revert each of the {@code keys} in {@code record} to {@code timestamp} by * creating new revisions that the relevant changes * that have occurred since {@code timestamp}. * * @param keys * @param record * @param timestamp */ @CompoundOperation public abstract void revert(Collection<String> keys, long record, Timestamp timestamp); /** * Revert {@code key} in each of the {@code records} to {@code timestamp} by * creating new revisions that the relevant changes that have occurred * since {@code timestamp}. * * @param key * @param records * @param timestamp */ @CompoundOperation public abstract void revert(String key, Collection<Long> records, Timestamp timestamp); /** * Atomically revert {@code key} in {@code record} to {@code timestamp} by * creating new revisions that undo the relevant changes that have * occurred since {@code timestamp}. * * @param key * @param record * @param timestamp */ public abstract void revert(String key, long record, Timestamp timestamp); /** * Search {@code key} for {@code query} and return the set of records that * match. * * @param key * @param query * @return the records that match the query */ public abstract Set<Long> search(String key, String query); /** * Set {@code key} as {@code value} in each of the {@code records}. * * @param key * @param value * @param records */ @CompoundOperation public abstract void set(String key, Object value, Collection<Long> records); /** * Atomically set {@code key} as {@code value} in {@code record}. This is a * convenience method that clears the values for {@code key} and adds * {@code value}. * * @param key * @param value * @param record */ public abstract <T> void set(String key, T value, long record); /** * Turn on {@code staging} mode so that all subsequent changes are * collected in a staging area before possibly being committed. Staged * operations are guaranteed to be reliable, all or nothing * units of work that allow correct recovery from failures and provide * isolation between clients so that Concourse is always in a consistent * state (e.g. a transaction). * <p> * After this method returns, all subsequent operations will be done in * {@code staging} mode until either {@link #abort()} or {@link #commit()} * is invoked. * </p> * <p> * All operations that occur within a transaction should be wrapped in a * try-catch block so that transaction exceptions can be caught and the * transaction can be properly aborted. * * <pre> * try { * concourse.stage(); * concourse.get("foo", 1); * concourse.add("foo", "bar", 1); * concourse.commit(); * } * catch (TransactionException e) { * concourse.abort(); * } * </pre> * * </p> */ public abstract void stage() throws TransactionException; /** * Remove link from {@code key} in {@code source} to {@code destination}. * * @param key * @param source * @param destination * @return {@code true} if the link is removed */ public abstract boolean unlink(String key, long source, long destination); /** * Verify {@code key} equals {@code value} in {@code record} and return * {@code true} if {@code value} is currently mapped from {@code key} in * {@code record}. * * @param key * @param value * @param record * @return {@code true} if {@code key} equals {@code value} in * {@code record} */ public abstract boolean verify(String key, Object value, long record); /** * Verify {@code key} equaled {@code value} in {@code record} at * {@code timestamp} and return {@code true} if {@code value} was mapped * from {@code key} in {@code record}. * * @param key * @param value * @param record * @param timestamp * @return {@code true} if {@code key} equaled {@code value} in * {@code record} at {@code timestamp} */ public abstract boolean verify(String key, Object value, long record, Timestamp timestamp); /** * Atomically verify {@code key} equals {@code expected} in {@code record} * and swap with {@code replacement}. * * @param key * @param expected * @param record * @param replacement * @return {@code true} if the swap is successful */ public abstract boolean verifyAndSwap(String key, Object expected, long record, Object replacement); /** * The implementation of the {@link Concourse} interface that establishes a * connection with the remote server and handles communication. This class * is a more user friendly wrapper around a Thrift * {@link ConcourseService.Client}. * * @author jnelson */ private final static class Client extends Concourse { // NOTE: The configuration variables are static because we want to // guarantee that they are set before the client connection is // constructed. Even though these variables are static, it is still the // case that any changes to the configuration will be picked up // immediately for new client connections. private static String SERVER_HOST; private static int SERVER_PORT; private static String USERNAME; private static String PASSWORD; private static String ENVIRONMENT; static { ConcourseConfiguration config; try { config = ConcourseConfiguration.loadConfig("concourse_client.prefs"); } catch (Exception e) { config = null; } SERVER_HOST = "localhost"; SERVER_PORT = 1717; USERNAME = "admin"; PASSWORD = "admin"; ENVIRONMENT = ""; if (config != null) { SERVER_HOST = config.getString("host", SERVER_HOST); SERVER_PORT = config.getInt("port", SERVER_PORT); USERNAME = config.getString("username", USERNAME); PASSWORD = config.getString("password", PASSWORD); ENVIRONMENT = config.getString("environment", ENVIRONMENT); } } /** * Represents a request to respond to a query using the current state as * opposed to the history. */ private static Timestamp now = Timestamp.fromMicros(0); /** * An encrypted copy of the username passed to the constructor. */ private final ByteBuffer username; /** * An encrypted copy of the password passed to the constructor. */ private final ByteBuffer password; /** * The host of the connection. */ private final String host; /** * The port of the connection. */ private final int port; /** * The environment to which the client is connected. */ private final String environment; /** * The Thrift client that actually handles all RPC communication. */ private final ConcourseService.Client client; /** * The client keeps a copy of its {@link AccessToken} and passes it to * the * server for each remote procedure call. The client will * re-authenticate * when necessary using the username/password read from the prefs file. */ private AccessToken creds = null; /** * Whenever the client starts a Transaction, it keeps a * {@link TransactionToken} so that the server can stage the changes in * the * appropriate place. */ private TransactionToken transaction = null; /** * Create a new Client connection to the environment of the Concourse * Server described in {@code concourse_client.prefs} (or the default * environment and server if the prefs file does not exist) and return a * handler to facilitate database interaction. */ public Client() { this(ENVIRONMENT); } /** * Create a new Client connection to the specified {@code environment} * of * the Concourse Server described in {@code concourse_client.prefs} (or * the default server if the prefs file does not exist) and return a * handler to facilitate database interaction. * * @param environment */ public Client(String environment) { this(SERVER_HOST, SERVER_PORT, USERNAME, PASSWORD, environment); } /** * Create a new Client connection to the default environment of the * specified Concourse Server and return a handler to facilitate * database interaction. * * @param host * @param port * @param username * @param password */ public Client(String host, int port, String username, String password) { this(host, port, username, password, ""); } /** * Create a new Client connection to the specified {@code environment} * of * the specified Concourse Server and return a handler to facilitate * database interaction. * * @param host * @param port * @param username * @param password * @param environment */ public Client(String host, int port, String username, String password, String environment) { this.host = host; this.port = port; this.username = ClientSecurity.encrypt(username); this.password = ClientSecurity.encrypt(password); this.environment = environment; final TTransport transport = new TSocket(host, port); try { transport.open(); TProtocol protocol = new TBinaryProtocol(transport); client = new ConcourseService.Client(protocol); authenticate(); Runtime.getRuntime().addShutdownHook(new Thread("shutdown") { @Override public void run() { if (transaction != null && transport.isOpen()) { abort(); } } }); } catch (TTransportException e) { throw new RuntimeException("Could not connect to the Concourse Server at " + host + ":" + port); } } @Override public void abort() { execute(new Callable<Void>() { @Override public Void call() throws Exception { if (transaction != null) { final TransactionToken token = transaction; transaction = null; client.abort(creds, token, environment); } return null; } }); } @Override public Map<Long, Boolean> add(String key, Object value, Collection<Long> records) { Map<Long, Boolean> result = TLinkedHashMap.newTLinkedHashMap("Record", "Result"); for (long record : records) { result.put(record, add(key, value, record)); } return result; } @Override public <T> boolean add(final String key, final T value, final long record) { if (!StringUtils.isBlank(key) && (!(value instanceof String) || (value instanceof String && !StringUtils.isBlank((String) value)))) { // CON-21 return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.add(key, Convert.javaToThrift(value), record, creds, transaction, environment); } }); } return false; } @Override public Map<Timestamp, String> audit(final long record) { return execute(new Callable<Map<Timestamp, String>>() { @Override public Map<Timestamp, String> call() throws Exception { Map<Long, String> audit = client.audit(record, null, creds, transaction, environment); return ((TLinkedHashMap<Timestamp, String>) Transformers.transformMap(audit, new Function<Long, Timestamp>() { @Override public Timestamp apply(Long input) { return Timestamp.fromMicros(input); } })).setKeyName("DateTime").setValueName("Revision"); } }); } @Override public Map<Timestamp, String> audit(final String key, final long record) { return execute(new Callable<Map<Timestamp, String>>() { @Override public Map<Timestamp, String> call() throws Exception { Map<Long, String> audit = client.audit(record, key, creds, transaction, environment); return ((TLinkedHashMap<Timestamp, String>) Transformers.transformMap(audit, new Function<Long, Timestamp>() { @Override public Timestamp apply(Long input) { return Timestamp.fromMicros(input); } })).setKeyName("DateTime").setValueName("Revision"); } }); } @CompoundOperation @Override public Map<Long, Map<String, Set<Object>>> browse(Collection<Long> records) { Map<Long, Map<String, Set<Object>>> data = TLinkedTableMap.newTLinkedTableMap("Record"); for (long record : records) { data.put(record, browse(record, now)); } return data; } @CompoundOperation @Override public Map<Long, Map<String, Set<Object>>> browse(Collection<Long> records, Timestamp timestamp) { Map<Long, Map<String, Set<Object>>> data = TLinkedTableMap.newTLinkedTableMap("Record"); for (long record : records) { data.put(record, browse(record, timestamp)); } return data; } @Override public Map<String, Set<Object>> browse(long record) { return browse(record, now); } @Override public Map<String, Set<Object>> browse(final long record, final Timestamp timestamp) { return execute(new Callable<Map<String, Set<Object>>>() { @Override public Map<String, Set<Object>> call() throws Exception { Map<String, Set<Object>> data = TLinkedHashMap.newTLinkedHashMap("Key", "Values"); for (Entry<String, Set<TObject>> entry : client .browse0(record, timestamp.getMicros(), creds, transaction, environment).entrySet()) { data.put(entry.getKey(), Transformers.transformSet(entry.getValue(), new Function<TObject, Object>() { @Override public Object apply(TObject input) { return Convert.thriftToJava(input); } })); } return data; } }); } @Override public Map<Object, Set<Long>> browse(String key) { return browse(key, now); } @Override public Map<Object, Set<Long>> browse(final String key, final Timestamp timestamp) { return execute(new Callable<Map<Object, Set<Long>>>() { @Override public Map<Object, Set<Long>> call() throws Exception { Map<Object, Set<Long>> data = TLinkedHashMap.newTLinkedHashMap(key, "Records"); for (Entry<TObject, Set<Long>> entry : client .browse1(key, timestamp.getMicros(), creds, transaction, environment).entrySet()) { data.put(Convert.thriftToJava(entry.getKey()), entry.getValue()); } return data; } }); } @Override public Map<Timestamp, Set<Object>> chronologize(final String key, final long record) { return execute(new Callable<Map<Timestamp, Set<Object>>>() { @Override public Map<Timestamp, Set<Object>> call() throws Exception { Map<Long, Set<TObject>> chronologize = client.chronologize(record, key, creds, transaction, environment); Map<Timestamp, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("DateTime", "Values"); for (Entry<Long, Set<TObject>> entry : chronologize.entrySet()) { result.put(Timestamp.fromMicros(entry.getKey()), Transformers.transformSet(entry.getValue(), new Function<TObject, Object>() { @Override public Object apply(TObject input) { return Convert.thriftToJava(input); } })); } return result; } }); } @Override public Map<Timestamp, Set<Object>> chronologize(final String key, final long record, final Timestamp start) { return chronologize(key, record, start, Timestamp.now()); } @Override public Map<Timestamp, Set<Object>> chronologize(final String key, final long record, final Timestamp start, final Timestamp end) { Preconditions.checkArgument(start.getMicros() <= end.getMicros(), "Start of range cannot be greater than the end"); Map<Timestamp, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("DateTime", "Values"); Map<Timestamp, Set<Object>> chronology = chronologize(key, record); int index = Timestamps.findNearestSuccessorForTimestamp(chronology.keySet(), start); Entry<Timestamp, Set<Object>> entry = null; if (index > 0) { entry = Iterables.get(chronology.entrySet(), index - 1); result.put(entry.getKey(), entry.getValue()); } for (int i = index; i < chronology.size(); i++) { entry = Iterables.get(chronology.entrySet(), i); if (entry.getKey().getMicros() >= end.getMicros()) { break; } result.put(entry.getKey(), entry.getValue()); } return result; } @Override public void clear(final Collection<Long> records) { for (Long record : records) { clear(record); } } @Override public void clear(Collection<String> keys, Collection<Long> records) { for (long record : records) { for (String key : keys) { clear(key, record); } } } @Override public void clear(Collection<String> keys, long record) { for (String key : keys) { clear(key, record); } } @Override public void clear(final long record) { execute(new Callable<Void>() { @Override public Void call() throws Exception { client.clear1(record, creds, transaction, environment); return null; } }); } @Override public void clear(String key, Collection<Long> records) { for (long record : records) { clear(key, record); } } @Override public void clear(final String key, final long record) { execute(new Callable<Void>() { @Override public Void call() throws Exception { client.clear(key, record, creds, transaction, environment); return null; } }); } @Override public boolean commit() { return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { final TransactionToken token = transaction; transaction = null; return client.commit(creds, token, environment); } }); } @Override public long create() { return Time.now(); // TODO get a primary key using a plugin } @Override public Map<Long, Set<String>> describe(Collection<Long> records) { Map<Long, Set<String>> result = TLinkedHashMap.newTLinkedHashMap("Record", "Keys"); for (long record : records) { result.put(record, describe(record)); } return result; } @Override public Map<Long, Set<String>> describe(Collection<Long> records, Timestamp timestamp) { Map<Long, Set<String>> result = TLinkedHashMap.newTLinkedHashMap("Record", "Keys"); for (long record : records) { result.put(record, describe(record, timestamp)); } return result; } @Override public Set<String> describe(long record) { return describe(record, now); } @Override public Set<String> describe(final long record, final Timestamp timestamp) { return execute(new Callable<Set<String>>() { @Override public Set<String> call() throws Exception { return client.describe(record, timestamp.getMicros(), creds, transaction, environment); } }); } @Override public void exit() { client.getInputProtocol().getTransport().close(); client.getOutputProtocol().getTransport().close(); } @Override public Map<Long, Map<String, Set<Object>>> fetch(Collection<String> keys, Collection<Long> records) { TLinkedTableMap<Long, String, Set<Object>> result = TLinkedTableMap .<Long, String, Set<Object>>newTLinkedTableMap("Record"); for (long record : records) { for (String key : keys) { result.put(record, key, fetch(key, record)); } } return result; } @Override public Map<Long, Map<String, Set<Object>>> fetch(Collection<String> keys, Collection<Long> records, Timestamp timestamp) { TLinkedTableMap<Long, String, Set<Object>> result = TLinkedTableMap .<Long, String, Set<Object>>newTLinkedTableMap("Record"); for (long record : records) { for (String key : keys) { result.put(record, key, fetch(key, record, timestamp)); } } return result; } @Override public Map<String, Set<Object>> fetch(Collection<String> keys, long record) { Map<String, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("Key", "Values"); for (String key : keys) { result.put(key, fetch(key, record)); } return result; } @Override public Map<String, Set<Object>> fetch(Collection<String> keys, long record, Timestamp timestamp) { Map<String, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("Key", "Values"); for (String key : keys) { result.put(key, fetch(key, record, timestamp)); } return result; } @Override public Map<Long, Set<Object>> fetch(String key, Collection<Long> records) { Map<Long, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("Record", key); for (long record : records) { result.put(record, fetch(key, record)); } return result; } @Override public Map<Long, Set<Object>> fetch(String key, Collection<Long> records, Timestamp timestamp) { Map<Long, Set<Object>> result = TLinkedHashMap.newTLinkedHashMap("Record", key); for (long record : records) { result.put(record, fetch(key, record, timestamp)); } return result; } @Override public Set<Object> fetch(String key, long record) { return fetch(key, record, now); } @Override public Set<Object> fetch(final String key, final long record, final Timestamp timestamp) { return execute(new Callable<Set<Object>>() { @Override public Set<Object> call() throws Exception { Set<TObject> values = client.fetch(key, record, timestamp.getMicros(), creds, transaction, environment); return Transformers.transformSet(values, new Function<TObject, Object>() { @Override public Object apply(TObject input) { return Convert.thriftToJava(input); } }); } }); } @Override public Set<Long> find(final Criteria criteria) { return execute(new Callable<Set<Long>>() { @Override public Set<Long> call() throws Exception { return client.find1(Translate.toThrift(criteria), creds, transaction, environment); } }); } @Override public Set<Long> find(Object object) { if (object instanceof BuildableState) { return find(((BuildableState) object).build()); } else { throw new IllegalArgumentException(object + " is not a valid argument for the find method"); } } @Override public Set<Long> find(String key, Operator operator, Object value) { return find(key, operator, value, now); } @Override public Set<Long> find(String key, Operator operator, Object value, Object value2) { return find(key, operator, value, value2, now); } @Override public Set<Long> find(final String key, final Operator operator, final Object value, final Object value2, final Timestamp timestamp) { return execute(new Callable<Set<Long>>() { @Override public Set<Long> call() throws Exception { return client.find(key, operator, Lists.transform(Lists.newArrayList(value, value2), new Function<Object, TObject>() { @Override public TObject apply(Object input) { return Convert.javaToThrift(input); } }), timestamp.getMicros(), creds, transaction, environment); } }); } @Override public Set<Long> find(final String key, final Operator operator, final Object value, final Timestamp timestamp) { return execute(new Callable<Set<Long>>() { @Override public Set<Long> call() throws Exception { return client.find(key, operator, Lists.transform(Lists.newArrayList(value), new Function<Object, TObject>() { @Override public TObject apply(Object input) { return Convert.javaToThrift(input); } }), timestamp.getMicros(), creds, transaction, environment); } }); } @Override public Map<Long, Map<String, Object>> get(Collection<String> keys, Collection<Long> records) { TLinkedTableMap<Long, String, Object> result = TLinkedTableMap .<Long, String, Object>newTLinkedTableMap("Record"); for (long record : records) { for (String key : keys) { Object value = get(key, record); if (value != null) { result.put(record, key, value); } } } return result; } @Override public Map<Long, Map<String, Object>> get(Collection<String> keys, Collection<Long> records, Timestamp timestamp) { TLinkedTableMap<Long, String, Object> result = TLinkedTableMap .<Long, String, Object>newTLinkedTableMap("Record"); for (long record : records) { for (String key : keys) { Object value = get(key, record, timestamp); if (value != null) { result.put(record, key, value); } } } return result; } @Override public Map<String, Object> get(Collection<String> keys, long record) { Map<String, Object> result = TLinkedHashMap.newTLinkedHashMap("Key", "Value"); for (String key : keys) { Object value = get(key, record); if (value != null) { result.put(key, value); } } return result; } @Override public Map<String, Object> get(Collection<String> keys, long record, Timestamp timestamp) { Map<String, Object> result = TLinkedHashMap.newTLinkedHashMap("Key", "Value"); for (String key : keys) { Object value = get(key, record, timestamp); if (value != null) { result.put(key, value); } } return result; } @Override public Map<Long, Object> get(String key, Collection<Long> records) { Map<Long, Object> result = TLinkedHashMap.newTLinkedHashMap("Record", key); for (long record : records) { Object value = get(key, record); if (value != null) { result.put(record, value); } } return result; } @Override public Map<Long, Object> get(String key, Collection<Long> records, Timestamp timestamp) { Map<Long, Object> result = TLinkedHashMap.newTLinkedHashMap("Record", key); for (long record : records) { Object value = get(key, record, timestamp); if (value != null) { result.put(record, value); } } return result; } @Override @Nullable public <T> T get(String key, long record) { return get(key, record, now); } @SuppressWarnings("unchecked") @Override @Nullable public <T> T get(String key, long record, Timestamp timestamp) { Set<Object> values = fetch(key, record, timestamp); if (!values.isEmpty()) { return (T) values.iterator().next(); } return null; } @Override public String getServerEnvironment() { return execute(new Callable<String>() { @Override public String call() throws Exception { return client.getServerEnvironment(creds, transaction, environment); } }); } @Override public String getServerVersion() { return execute(new Callable<String>() { @Override public String call() throws Exception { return client.getServerVersion(); } }); } @Override public long insert(final String json) { return execute(new Callable<Long>() { @Override public Long call() throws Exception { return client.insert1(json, creds, transaction, environment); } }); } @Override public Map<Long, Boolean> insert(String json, Collection<Long> records) { Map<Long, Boolean> result = TLinkedHashMap.newTLinkedHashMap("Record", "Result"); for (long record : records) { result.put(record, insert(json, record)); } return result; } @Override public boolean insert(final String json, final long record) { return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.insert(json, record, creds, transaction, environment); } }); } @Override public Map<Long, Boolean> link(String key, long source, Collection<Long> destinations) { Map<Long, Boolean> result = TLinkedHashMap.newTLinkedHashMap("Record", "Result"); for (long destination : destinations) { result.put(destination, link(key, source, destination)); } return result; } @Override public boolean link(String key, long source, long destination) { return add(key, Link.to(destination), source); } @Override public Map<Long, Boolean> ping(Collection<Long> records) { Map<Long, Boolean> result = TLinkedHashMap.newTLinkedHashMap("Record", "Result"); for (long record : records) { result.put(record, ping(record)); } return result; } @Override public boolean ping(final long record) { return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.ping(record, creds, transaction, environment); } }); } @Override public Map<Long, Boolean> remove(String key, Object value, Collection<Long> records) { Map<Long, Boolean> result = TLinkedHashMap.newTLinkedHashMap("Record", "Result"); for (long record : records) { result.put(record, remove(key, value, record)); } return result; } @Override public <T> boolean remove(final String key, final T value, final long record) { if (!StringUtils.isBlank(key) && (!(value instanceof String) || (value instanceof String && !StringUtils.isBlank((String) value)))) { // CON-21 return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.remove(key, Convert.javaToThrift(value), record, creds, transaction, environment); } }); } return false; } @Override public void revert(Collection<String> keys, Collection<Long> records, Timestamp timestamp) { for (long record : records) { for (String key : keys) { revert(key, record, timestamp); } } } @Override public void revert(Collection<String> keys, long record, Timestamp timestamp) { for (String key : keys) { revert(key, record, timestamp); } } @Override public void revert(String key, Collection<Long> records, Timestamp timestamp) { for (long record : records) { revert(key, record, timestamp); } } @Override public void revert(final String key, final long record, final Timestamp timestamp) { execute(new Callable<Void>() { @Override public Void call() throws Exception { client.revert(key, record, timestamp.getMicros(), creds, transaction, environment); return null; } }); } @Override public Set<Long> search(final String key, final String query) { return execute(new Callable<Set<Long>>() { @Override public Set<Long> call() throws Exception { return client.search(key, query, creds, transaction, environment); } }); } @Override public void set(String key, Object value, Collection<Long> records) { for (long record : records) { set(key, value, record); } } @Override public <T> void set(final String key, final T value, final long record) { execute(new Callable<Void>() { @Override public Void call() throws Exception { client.set0(key, Convert.javaToThrift(value), record, creds, transaction, environment); return null; } }); } @Override public void stage() throws TransactionException { execute(new Callable<Void>() { @Override public Void call() throws Exception { transaction = client.stage(creds, environment); return null; } }); } @Override public String toString() { return "Connected to " + host + ":" + port + " as " + new String(ClientSecurity.decrypt(username).array()); } @Override public boolean unlink(String key, long source, long destination) { return remove(key, Link.to(destination), source); } @Override public boolean verify(String key, Object value, long record) { return verify(key, value, record, now); } @Override public boolean verify(final String key, final Object value, final long record, final Timestamp timestamp) { return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.verify(key, Convert.javaToThrift(value), record, timestamp.getMicros(), creds, transaction, environment); } }); } @Override public boolean verifyAndSwap(final String key, final Object expected, final long record, final Object replacement) { return execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return client.verifyAndSwap(key, Convert.javaToThrift(expected), record, Convert.javaToThrift(replacement), creds, transaction, environment); } }); } /** * Authenticate the {@link #username} and {@link #password} and populate * {@link #creds} with the appropriate AccessToken. */ private void authenticate() { try { creds = client.login(ClientSecurity.decrypt(username), ClientSecurity.decrypt(password), environment); } catch (TException e) { throw Throwables.propagate(e); } } /** * Execute the task defined in {@code callable}. This method contains * retry logic to handle cases when {@code creds} expires and must be * updated. * * @param callable * @return the task result */ private <T> T execute(Callable<T> callable) { try { return callable.call(); } catch (SecurityException e) { authenticate(); return execute(callable); } catch (TTransactionException e) { throw new TransactionException(); } catch (Exception e) { throw Throwables.propagate(e); } } } }