org.cinchapi.runway.Record Maven / Gradle / Ivy
Show all versions of runway Show documentation
package org.cinchapi.runway;
import gnu.trove.map.TLongObjectMap;
import gnu.trove.map.hash.TLongObjectHashMap;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import org.cinchapi.concourse.Concourse;
import org.cinchapi.concourse.ConnectionPool;
import org.cinchapi.concourse.Link;
import org.cinchapi.concourse.Tag;
import org.cinchapi.concourse.TransactionException;
import org.cinchapi.concourse.lang.BuildableState;
import org.cinchapi.concourse.lang.Criteria;
import org.cinchapi.concourse.server.io.Serializables;
import org.cinchapi.concourse.thrift.Operator;
import org.cinchapi.concourse.time.Time;
import org.cinchapi.concourse.util.ByteBuffers;
import org.cinchapi.runway.util.AnyObject;
import org.cinchapi.runway.validation.Validator;
import static com.google.common.base.Preconditions.checkState;
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.Sets;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* A {@link Record} is a a wrapper around the same in {@link Concourse} that
* facilitates object-oriented interaction while automatically preserving
* transactional security.
*
* Each subclass defines its "schema" through {@code non-transient} member
* variables. When a Record is loaded from Concourse, the member variables are
* automatically populated with information from the database. And, when a
* Record is {@link #save() saved}, the values of those variables (including any
* changes) will automatically be stored/updated in Concourse.
*
* Variable Modifiers
*
* Records will respect the native java modifiers placed on variables. This
* means that transient fields are never stored or loaded from the database.
* And, private fields are never included in a JSON {@link #dump()} or
* {@link #toString()} output.
*
* Creating a new Instance
*
* Records are created by calling the {@link Record#create(Class)} method and
* supplying the desired class for the instance. Internally, the create routine
* calls the no-arg constructor so subclasses should never provide their own
* constructors.
*
*
* @author jnelson
*
*/
public abstract class Record {
/**
* Atomically clear all of the records in {@code clazz} from the database.
*
* @param clazz
*/
public static void clear(Class clazz) {
Concourse concourse = connections().request();
try {
concourse.stage();
Set records = concourse.find(Criteria.where()
.key(SECTION_KEY).operator(Operator.EQUALS)
.value(clazz.getName()));
for (long record : records) {
concourse.clear(record);
}
concourse.commit();
}
catch (TransactionException e) {
concourse.abort();
clear(clazz);
}
finally {
connections().release(concourse);
}
}
/**
* Create a new {@link Record} that is contained within the specified
* {@code clazz}.
*
* @param clazz
* @return the new Record
*/
public static T create(Class clazz) {
T record = getNewDefaultInstance(clazz);
record.init();
return record;
}
/**
* Find any return any records in {@code clazz} that match {@code criteria}.
*
* @param clazz
* @param criteria
* @return the records that match {@code criteria}
*/
public static Set find(Class clazz,
BuildableState criteria) {
return find(clazz, criteria.build());
}
/**
* Find any return any records in {@code clazz} that match {@code criteria}.
*
* @param clazz
* @param criteria
* @return the records that match {@code criteria}
*/
public static Set find(Class clazz,
Criteria criteria) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.find(criteria);
for (long id : ids) {
if(inClass(id, clazz, concourse)) {
records.add(load(clazz, id));
}
}
return records;
}
finally {
connections().release(concourse);
}
}
/**
* Find and return all records that match {@code criteria}, regardless of
* what class the Record is contained within.
*
* @param criteria
* @return the records that match {@code criteria}
*/
public static Set findAll(BuildableState criteria) {
return findAll(criteria.build());
}
/**
* Find and return all records that match {@code criteria}, regardless of
* what class the Record is contained within.
*
* @param criteria
* @return the records that match {@code criteria}
*/
@SuppressWarnings("unchecked")
public static Set findAll(Criteria criteria) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.find(criteria);
for (long id : ids) {
Class extends Record> clazz = (Class extends Record>) Class
.forName((String) concourse.get(SECTION_KEY, id));
records.add(load(clazz, id));
}
return records;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
finally {
connections().release(concourse);
}
}
/**
* Find and return every record in {@code clazz} or any of its children that
* match {@code criteria}.
*
* @param clazz
* @param criteria
* @return the records that match {@code criteria}
*/
public static Set extends T> findEvery(Class clazz,
BuildableState criteria) {
return findEvery(clazz, criteria.build());
}
/**
* Find and return every record in {@code clazz} or any of its children that
* match {@code criteria}.
*
* @param clazz
* @param criteria
* @return the records that match {@code criteria}
*/
@SuppressWarnings("unchecked")
public static Set extends T> findEvery(Class clazz,
Criteria criteria) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.find(criteria);
for (long id : ids) {
Class extends T> c = (Class extends T>) Class
.forName((String) concourse.get(SECTION_KEY, id));
if(clazz.isAssignableFrom(c)) {
records.add(load(c, id));
}
}
return records;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
finally {
connections().release(concourse);
}
}
/**
* Find and return the ids of any records in {@code clazz} that match
* {@code criteria}.
*
* @param clazz
* @param criteria
* @return the ids of the records that match {@code criteria}
*/
public static Set findIds(Class clazz,
BuildableState criteria) {
return findIds(clazz, criteria.build());
}
/**
* Find and return the ids of any records in {@code clazz} that match
* {@code criteria}.
*
* @param clazz
* @param criteria
* @return the ids of the records that match {@code criteria}
*/
public static Set findIds(Class clazz,
Criteria criteria) {
Concourse concourse = connections().request();
try {
Set ids = concourse.find(criteria);
Iterator it = ids.iterator();
while (it.hasNext()) {
long id = it.next();
if(!inClass(id, clazz, concourse)) {
it.remove();
}
}
return ids;
}
finally {
connections().release(concourse);
}
}
/**
* Return the single instance of {@code clazz} that matches the
* {@code criteria} or {@code null} if it doesn't exist or multiple objects
* match.
*
* @param clazz
* @param criteria
* @return the single matching result
*/
@Nullable
public static T findSingleInstance(Class clazz,
BuildableState criteria) {
return findSingleInstance(clazz, criteria.build());
}
/**
* Return the single instance of {@code clazz} that matches the
* {@code criteria} or {@code null} if it doesn't exist or multiple objects
* match.
*
* @param clazz
* @param criteria
* @return the single matching result
*/
@Nullable
public static T findSingleInstance(Class clazz,
Criteria criteria) {
Set results = find(clazz, criteria);
try {
return Iterables.getOnlyElement(results);
}
catch (Exception e) {
return null;
}
}
/**
* Return a list of all the ids for every record in {@code clazz}. This
* method does to load up the actual records.
*
* @param clazz
* @return the ids of all the records in the class
*/
public static Set getEveryId(Class clazz) {
Concourse concourse = connections().request();
try {
return concourse.find(Criteria.where().key(SECTION_KEY)
.operator(Operator.EQUALS).value(clazz.getName()));
}
finally {
connections().release(concourse);
}
}
/**
* Load the Record that is contained within the specified {@code clazz} and
* has the specified {@code id}.
*
* Multiple calls to this method with the same parameters will return
* different instances (e.g. the instances are not cached).
* This is done deliberately so different threads/clients can make changes
* to a Record in isolation.
*
*
* @param clazz
* @param id
* @return the existing Record
*/
public static T load(Class clazz, long id) {
return load(clazz, id, new TLongObjectHashMap());
}
/**
* Load every record in {@code clazz}.
*
* @param clazz
* @return all the records in the class
*/
public static Set loadEvery(Class clazz) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.find(Criteria.where().key(SECTION_KEY)
.operator(Operator.EQUALS).value(clazz.getName()));
for (long id : ids) {
try {
records.add(load(clazz, id));
}
catch (ZombieException e) {
continue;
}
}
return records;
}
finally {
connections().release(concourse);
}
}
/**
* Save all the changes in all of the {@code records} using a single ACID
* transaction.
*
* @param records
* @return {@code true} if all the changes are atomically saved.
*/
public static boolean saveAll(Record... records) {
Concourse concourse = connections().request();
long transactionId = Time.now();
Record current = null;
try {
concourse.stage();
concourse.set("transaction_id", transactionId, METADATA_RECORD);
Set waiting = Sets.newHashSet(records);
waitingToBeSaved.put(transactionId, waiting);
for (Record record : records) {
current = record;
record.save(concourse);
}
concourse.clear("transaction_id", METADATA_RECORD);
return concourse.commit();
}
catch (Throwable t) {
concourse.abort();
if(current != null) {
current.errors.add(Throwables.getStackTraceAsString(t));
}
return false;
}
finally {
waitingToBeSaved.remove(transactionId);
connections().release(concourse);
}
}
/**
* Search and return any records in {@code clazz} that match the search
* {@code query} for {@code key}.
*
* @param clazz
* @param key
* @param query
* @return the records that match the search
*/
public static Set search(Class clazz, String key,
String query) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.search(key, query);
for (long id : ids) {
if(inClass(id, clazz, concourse)) {
records.add(load(clazz, id));
}
}
return records;
}
finally {
connections().release(concourse);
}
}
/**
* Search and return all records that match the search {@code query} for
* {@code key}, regardless of what class the Record is contained within.
*
* @param key
* @param query
* @return the records that match the search
*/
@SuppressWarnings("unchecked")
public static Set searchAll(String key, String query) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.search(key, query);
for (long id : ids) {
Class extends Record> clazz = (Class extends Record>) Class
.forName((String) concourse.get(SECTION_KEY, id));
records.add(load(clazz, id));
}
return records;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
finally {
connections().release(concourse);
}
}
/**
* Search and return every records in {@code clazz} or any of its children
* that match the search {@code query} for {@code key}.
*
* @param clazz
* @param key
* @param query
* @return the records that match the search
*/
@SuppressWarnings("unchecked")
public static Set extends T> searchEvery(
Class clazz, String key, String query) {
Concourse concourse = connections().request();
try {
Set records = Sets.newLinkedHashSet();
Set ids = concourse.search(key, query);
for (long id : ids) {
Class extends T> c = (Class extends T>) Class
.forName((String) concourse.get(SECTION_KEY, id));
if(clazz.isAssignableFrom(c)) {
records.add(load(c, id));
}
}
return records;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
finally {
connections().release(concourse);
}
}
/**
* Search for the single instance of {@code clazz} that matches the search
* {@code criteria} or return {@code null} if it doesn't exist or multiple
* objects match.
*
* @param clazz
* @param key
* @param value
* @return the single matching search result
*/
@Nullable
public static T searchSingleInstance(Class clazz,
String key, String value) {
Set results = search(clazz, key, value);
try {
return Iterables.getOnlyElement(results);
}
catch (Exception e) {
return null;
}
}
/**
* Set the connection information that is used for Concourse.
*
* @param host
* @param port
* @param username
* @param password
*/
public static void setConnectionInformation(String host, int port,
String username, String password) { // visible for testing
connections = ConnectionPool.newCachedConnectionPool(host, port,
username, password);
}
/**
* Return a handler to the connection pool that is in use.
*
* @return the connection pool handler
*/
private static ConnectionPool connections() {
if(connections == null) {
connections = ConnectionPool
.newCachedConnectionPool("concourse_client.prefs");
}
return connections;
}
/**
* Get a new instance of {@code clazz} by calling the default (zero-arg)
* constructor, if it exists. This method attempts to correctly invoke
* constructors for nested inner classes.
*
* @param clazz
* @return the instance of the {@code clazz}.
*/
@SuppressWarnings("unchecked")
private static T getNewDefaultInstance(Class clazz) {
try {
Class> enclosingClass = clazz.getEnclosingClass();
if(enclosingClass != null) {
Object enclosingInstance = getNewDefaultInstance(enclosingClass);
Constructor> constructor = clazz
.getDeclaredConstructor(enclosingClass);
return (T) constructor.newInstance(enclosingInstance);
}
else {
return clazz.newInstance();
}
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
/**
* Return {@code true} if the Record identified by {@code id} is a member of
* {@code clazz}.
*
* @param id
* @param clazz
* @param concourse
* @return {@code true} if the record is in the class
*/
private static boolean inClass(long id, Class extends Record> clazz,
@Nullable Concourse concourse) {
if(concourse == null) {
concourse = connections().request();
try {
return inClass(id, clazz, concourse);
}
finally {
connections().release(concourse);
}
}
else {
try {
return ((String) concourse.get(SECTION_KEY, id)).equals(clazz
.getName());
}
catch (NullPointerException e) { // NPE indicates the record does
// not have a SECTION_KEY
return false;
}
}
}
/**
* Return {@code true} if the record identified by {@code id} is in a
* "zombie" state meaning it exists in the database without any actual data.
*
* @param id
* @param concourse
* @return {@code true} if the record is a zombie
*/
private static boolean inZombieState(long id, Concourse concourse) {
return concourse.describe(id).equals(ZOMBIE_DESCRIPTION);
}
/**
* Return {@code true} if {@code record} is part of a single transaction
* within {@code concourse} and is waiting to be saved.
*
* @param concourse
* @param record
* @return {@code true} if the record is waiting to be saved
*/
private static boolean isWaitingToBeSaved(Concourse concourse, Record record) {
try {
long transactionId = concourse.get("transaction_id",
METADATA_RECORD);
return waitingToBeSaved.get(transactionId).contains(record);
}
catch (NullPointerException e) {
return false;
}
}
/**
* Convert a generic {@code object} to the appropriate {@link JsonElement}.
*
* This method accepts {@link Iterable} collections and recursively
* transforms them to JSON arrays.
*
*
* @param object
* @return the appropriate JsonElement
*/
private static JsonElement jsonify(Object object, Set seen) {
if(object instanceof Iterable
&& Iterables.size((Iterable>) object) == 1) {
return jsonify(Iterables.getOnlyElement((Iterable>) object), seen);
}
else if(object instanceof Iterable) {
JsonArray array = new JsonArray();
for (Object element : (Iterable>) object) {
array.add(jsonify(element, seen));
}
return array;
}
else if(object instanceof Number) {
return new JsonPrimitive((Number) object);
}
else if(object instanceof Boolean) {
return new JsonPrimitive((Boolean) object);
}
else if(object instanceof String) {
return new JsonPrimitive((String) object);
}
else if(object instanceof Tag) {
return new JsonPrimitive((String) object.toString());
}
else if(object instanceof Record) {
if(!seen.contains(object)) {
seen.add((Record) object);
return ((Record) object).toJsonElement(seen);
}
else {
return jsonify(((Record) object).getId() + " (recursive link)",
seen);
}
}
else {
Gson gson = new Gson();
return gson.toJsonTree(object);
}
}
/**
* Internal method to help recursively load records by keeping tracking of
* which ones currently exist. Ultimately this method will load the Record
* that is contained within the specified {@code clazz} and
* has the specified {@code id}.
*
* Multiple calls to this method with the same parameters will return
* different instances (e.g. the instances are not cached).
* This is done deliberately so different threads/clients can make changes
* to a Record in isolation.
*
*
* @param clazz
* @param id
* @param existing
* @return
*/
private static T load(Class clazz, long id,
TLongObjectMap existing) {
T record = getNewDefaultInstance(clazz);
record.load(id, existing);
return record;
}
/**
* The connection pool. Access using the {@link #connections()} method.
*/
private static ConnectionPool connections;
/**
* The record where metadata is stored. We typically store some transient
* metadata for transaction routing within this record (so its only visible
* within the specific transaction) and we clear it before commit time.
*/
private static long METADATA_RECORD = -1;
/**
* The key used to hold the section metadata.
*/
private static final String SECTION_KEY = "_"; // just want a simple/short
// key name that is likely to
// avoid collisions
/**
* A mapping from a transaction id to the set of records that are waiting to
* be saved within that transaction. We use this collection to ensure that a
* record being saved only links to an existing record in the database or a
* record that will later exist (e.g. waiting to be saved).
*/
private static final TLongObjectMap> waitingToBeSaved = new TLongObjectHashMap>();
/**
* The description of a record that is considered to be in "zombie" state.
*/
private static final Set ZOMBIE_DESCRIPTION = Sets
.newHashSet(SECTION_KEY);
static {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
connections.close();
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
});
}
/**
* The variable that holds the name of the section in the database where
* this record is stored.
*/
private transient String _ = getClass().getName();
/**
* A flag that indicates if the record has been deleted using the
* {@link #deleteOnSave()} method.
*/
private transient boolean deleted = false;
/**
* A log of any suppressed errors related to this Record. The descriptions
* of these errors can be thrown at any point from the
* {@link #throwSupressedExceptions()} method.
*/
private transient List errors = Lists.newArrayList();
/**
* A cache of all the fields in this class and all of its parents.
*/
private transient Field[] fields0;
/**
* The primary key that is used to identify this Record in the database.
*/
private transient long id;
/**
* A flag that indicates this Record is in violation of some constraint and
* therefore cannot be used without ruining the integrity of the database.
*/
private transient boolean inViolation = false;
/**
* A flag that indicates that the Record object has either been initialized
* as a new record in the database or has loaded an existing record and is
* therefore usable.
*/
private transient boolean usable = false;
/*
* (non-Javadoc)
* Create a new Record instance. In order for this to be operable, a call
* must be made to either {@link #init()} or {@link #load(long)}. A caller
* should never invoke this constructor directly because it merely creates a
* hallow shell object that is useless until a call is made to either #init
* or #load.
*/
/**
* DO NOT CALL and DO NOT OVERRIDE!!! Please read the documentation for this
* class for appropriate instructions on instantiating Record instances.
*/
protected Record() {/* noop */}
/**
* Delete this {@link Record} from Concourse when the {@link #save()} method
* is called.
*/
public void deleteOnSave() {
deleted = true;
}
/**
* Dump the non private data in this {@link Record} as a JSON string.
*
* @return the JSON string
*/
public String dump() {
return toJsonElement().toString();
}
/**
* Dump the non private specified {@code} keys in this {@link Record} as a
* JSON string.
*
* @param keys
* @return the json string
*/
public String dump(String... keys) {
return toJsonElement(keys).toString();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Record) {
return id == ((Record) obj).id;
}
else {
return false;
}
}
/**
* Return a map that contains all of the non-private data in this record.
* For example, this method can be used to return data that can be sent to a
* template processor to map values to front-end variables. You can use the
* {@link #getMoreData()} method to define additional data values.
*
* @return the data in this record
*/
public Map getData() {
try {
Map data = getMoreData();
Field[] fields = getAllDeclaredFields();
data.put("id", id);
for (Field field : fields) {
Object value;
if(!Modifier.isPrivate(field.getModifiers())
&& !Modifier.isTransient(field.getModifiers())
&& (value = field.get(this)) != null) {
data.put(field.getName(), value);
}
}
return data;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
/**
* Return a map that contains all of the non-private data in this record
* based on the specified {@code keys}. For example, this method can be used
* to return data that can be sent to a template processor to map values to
* front-end variables. You can use the {@link #getMoreData()} method to
* define
* additional data values, and the keys that map to those values will only
* be returned if they are included in {@code keys}.
*
* @return the data in this record
*/
public Map getData(String... keys) {
try {
Map data = getMoreData();
Set _keys = Sets.newHashSet(keys);
for (String key : data.keySet()) {
if(!_keys.contains(key)) {
data.remove(key);
}
}
Field[] fields = getAllDeclaredFields();
data.put("id", id);
for (Field field : fields) {
Object value;
if(_keys.contains(field.getName())
&& !Modifier.isPrivate(field.getModifiers())
&& !Modifier.isTransient(field.getModifiers())
&& (value = field.get(this)) != null) {
data.put(field.getName(), value);
}
}
return data;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
/**
* Return the {@link #id} that uniquely identifies this record.
*
* @return the id
*/
public final long getId() {
return id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
/**
* Save all changes that have been made to this record using an ACID
* transaction.
*
* Use {@link Record#saveAll(Record...)} to save changes in multiple records
* in a single ACID transaction.
*
*
* @return {@code true} if all the changes have been atomically saved.
*/
public final boolean save() {
Concourse concourse = connections().request();
try {
Preconditions.checkState(!inViolation);
errors.clear();
concourse.stage();
if(deleted) {
delete(concourse);
}
else {
save(concourse);
}
return concourse.commit();
}
catch (Throwable t) {
concourse.abort();
if(inZombieState(concourse)) {
concourse.clear(id);
}
errors.add(Throwables.getStackTraceAsString(t));
return false;
}
finally {
connections().release(concourse);
}
}
/**
* Thrown an exception that describes any exceptions that were previously
* suppressed. If none occured, then this method does nothing. This is a
* good way to understand why a save operation fails.
*
* @throws RuntimeException
*/
public void throwSupressedExceptions() {
if(!errors.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String error : errors) {
sb.append(error);
sb.append(System.getProperty("line.separator"));
}
throw new RuntimeException(sb.toString());
}
}
/**
* Returns a {@link JsonElement} representation of this record which
* includes all of its non private fields.
*
* @return the JsonElement representation
*/
public JsonElement toJsonElement() {
return toJsonElement(Sets. newHashSet());
}
/**
* Returns a {@link JsonElement} representation of this record which
* includes all of the non private {@code keys} that are specified.
*
* @param keys
* @return the JsonElement representation
*/
public JsonElement toJsonElement(String... keys) {
return toJsonElement(Sets. newHashSet(), keys);
}
@Override
public final String toString() {
return dump();
}
/**
* Provide additional data about this Record that might not be encapsulated
* in its fields. For example, this is a good way to provide template
* specific information that isn't persisted to the database.
*
* @return the additional data
*/
protected Map getMoreData() {
return Maps.newHashMap();
}
/**
* Initialize the new record with all the core data.
*/
final void init() { // visible for access from static #init()
// method
if(!usable) {
Concourse concourse = connections().request();
try {
this.id = concourse.insert(initData());
usable = true;
// TODO set initial state
}
finally {
connections().release(concourse);
}
}
}
/**
* Load an existing record from the database and add all of it to this
* instance in memory.
*
* @param id
* @param existing
*/
@SuppressWarnings({ "unchecked", "rawtypes", })
final void load(long id, TLongObjectMap existing) { // visible for
// access
// from static
// #load
// method
if(!usable) {
this.id = id;
existing.put(id, this); // add the current object so we don't
// recurse infinitely
Concourse concourse = connections().request();
checkConstraints(concourse);
try {
if(inZombieState(id, concourse)) {
concourse.clear(id);
throw new ZombieException();
}
Field[] fields = getAllDeclaredFields();
for (Field field : fields) {
if(!Modifier.isTransient(field.getModifiers())) {
String key = field.getName();
if(Record.class.isAssignableFrom(field.getType())) {
Record record = (Record) getNewDefaultInstance(field
.getType());
record.load(
((Link) concourse.get(key, id)).longValue(),
existing);
field.set(this, record);
}
else if(Collection.class.isAssignableFrom(field
.getType())) {
Collection collection = null;
if(Modifier.isAbstract(field.getType()
.getModifiers())
|| Modifier.isInterface(field.getType()
.getModifiers())) {
if(field.getType() == Set.class) {
collection = Sets.newLinkedHashSet();
}
else { // assume list
collection = Lists.newArrayList();
}
}
else {
collection = (Collection) field.getType()
.newInstance();
}
Set> values = concourse.fetch(key, id);
for (Object item : values) {
if(item instanceof Link) {
long link = ((Link) item).longValue();
Record obj = existing.get(link);
if(obj == null) {
String section = concourse.get("_",
link);
if(Strings.isNullOrEmpty(section)) {
concourse.remove(key, item, id);
continue;
}
else {
Class extends Record> linkClass = (Class extends Record>) Class
.forName(section.toString());
item = load(linkClass, link,
existing);
}
}
else {
item = obj;
}
}
collection.add(item);
}
field.set(this, collection);
}
else if(field.getType().isArray()) {
List list = new ArrayList();
Set> values = concourse.fetch(key, id);
for (Object item : values) {
list.add(item);
}
field.set(this, list.toArray());
}
else if(field.getType().isPrimitive()
|| field.getType() == String.class
|| field.getType() == Integer.class
|| field.getType() == Long.class
|| field.getType() == Float.class
|| field.getType() == Double.class) {
Object value = concourse.get(key, id);
if(value != null) { // Java doesn't allow primitive
// types to hold nulls
field.set(this, concourse.get(key, id));
}
}
else if(field.getType() == Tag.class) {
Object object = concourse.get(key, id);
if(object != null) {
field.set(this, Tag.create((String) object));
}
}
else if(field.getType().isEnum()) {
String stored = concourse.get(key, id);
if(stored != null) {
field.set(this, Enum.valueOf(
(Class) field.getType(),
stored.toString()));
}
}
else if(Serializable.class.isAssignableFrom(field
.getType())) {
String base64 = concourse.get(key, id);
if(base64 != null) {
ByteBuffer bytes = ByteBuffer.wrap(BaseEncoding
.base64Url().decode(base64));
field.set(this, Serializables.read(bytes,
(Class) field.getType()));
}
}
else {
Gson gson = new Gson();
Object object = gson.fromJson(
(String) concourse.get(key, id),
field.getType());
field.set(this, object);
}
}
}
usable = true;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
finally {
connections().release(concourse);
}
}
}
/**
* Check to ensure that this Record does not violate any constraints. If so,
* throw an {@link IllegalStateException}.
*
* @param concourse
* @throws IllegalStateException
*/
private void checkConstraints(Concourse concourse) {
try {
String section = concourse.get(SECTION_KEY, id);
checkState(section != null);
checkState(
section.equals(_)
|| Class.forName(_).isAssignableFrom(
Class.forName(section)),
"Cannot load a record from section %s "
+ "into a Record of type %s", section, _);
}
catch (ReflectiveOperationException | IllegalStateException e) {
inViolation = true;
throw Throwables.propagate(e);
}
}
/**
* Perform an actual "deletion" of this record from the database.
*
* @param concourse
*/
private void delete(Concourse concourse) {
concourse.clear(id);
}
/**
* Get all the fields that are declared in this class and any of its
* parents.
*
* @return the declared fields
*/
private final Field[] getAllDeclaredFields() {
if(fields0 == null) {
List fields = Lists.newArrayList();
Class> clazz = this.getClass();
while (clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
if(!field.getName().equalsIgnoreCase("fields0")
&& !field.isSynthetic()
&& !Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
fields.add(field);
}
}
clazz = clazz.getSuperclass();
}
fields0 = fields.toArray(new Field[] {});
}
return fields0;
}
/**
* Return a JSON string that contains all the data which initially populates
* each record upon creation.
*
* @return the initial data
*/
private final String initData() {
JsonObject object = new JsonObject();
object.addProperty(SECTION_KEY, "`" + _ + "`"); // Wrap the
// #section with
// `` so that it
// is stored in
// Concourse as
// a Tag.
return object.toString();
}
/**
* Return {@code true} if this record is in a "zombie" state meaning it
* exists in the database without any actual data.
*
* @param concourse
* @return {@code true} if this record is a zombie
*/
private final boolean inZombieState(Concourse concourse) {
return inZombieState(id, concourse);
}
/**
* Return {@code true} if {@code key} as {@code value} for this class is
* unique, meaning there is no other record in the database in this class
* with that mapping. If {@code value} is a collection, then this method
* will return {@code true} if and only if every element in the collection
* is unique.
*
* @param concourse
* @param key
* @param value
* @return {@code true} if {@code key} as {@code value} is a unique mapping
* for this class
*/
private boolean isUnique(Concourse concourse, String key, Object value) {
if(value instanceof Iterable> || value.getClass().isArray()) {
for (Object obj : (Iterable>) value) {
if(!isUnique(concourse, key, obj)) {
return false;
}
}
return true;
}
else {
Criteria criteria = Criteria.where().key(SECTION_KEY)
.operator(Operator.EQUALS).value(_).and().key(key)
.operator(Operator.EQUALS).value(value).build();
Set records = concourse.find(criteria);
return records.isEmpty()
|| (records.contains(id) && records.size() == 1);
}
}
/**
* Save the data in this record using the specified {@code concourse}
* connection. This method assumes that the caller has already started an
* transaction, if necessary and will commit the transaction after this
* method completes.
*
* @param concourse
*/
private void save(final Concourse concourse) {
try {
Field[] fields = getAllDeclaredFields();
for (Field field : fields) {
if(!Modifier.isTransient(field.getModifiers())) {
field.setAccessible(true);
final String key = field.getName();
final Object value = field.get(this);
if(field.isAnnotationPresent(ValidatedBy.class)) {
Class extends Validator> validatorClass = field
.getAnnotation(ValidatedBy.class).value();
Validator validator = getNewDefaultInstance(validatorClass);
Preconditions.checkState(validator.validate(value),
validator.getErrorMessage());
}
if(field.isAnnotationPresent(Unique.class)) {
Preconditions.checkState(
isUnique(concourse, key, value),
field.getName() + " must be unique");
}
if(field.isAnnotationPresent(Required.class)) {
Preconditions.checkState(
!AnyObject.isNullOrEmpty(value),
field.getName() + " is required");
}
if(value != null) {
store(key, value, concourse, false);
}
}
}
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
/**
* Store {@code key} as {@code value} using the specified {@code concourse}
* connection. The {@code append} flag is used to indicate if the value
* should be appended using the {@link Concourse#add(String, Object, long)}
* method or inserted using the {@link Concourse#set(String, Object, long)}
* method.
*
* @param key
* @param value
* @param concourse
* @param append
*/
@SuppressWarnings("rawtypes")
private void store(String key, Object value, Concourse concourse,
boolean append) {
// TODO: dirty field detection!
if(value instanceof Record) {
Record record = (Record) value;
Preconditions.checkState(!record.inZombieState(concourse)
|| isWaitingToBeSaved(concourse, record),
"Cannot link to an empty record! "
+ "You must save the record to "
+ "which you're linking before "
+ "saving this record, or save "
+ "them both within an atomic transaction "
+ "using the Record#saveAll() method");
concourse.link(key, id, record.id);
}
else if(value instanceof Collection || value.getClass().isArray()) {
// TODO use reconcile() function once 0.5.0 comes out...
concourse.clear(key, id); // TODO this is extreme...move to a diff
// based approach to delete only values
// that should be deleted
for (Object item : (Iterable>) value) {
store(key, item, concourse, true);
}
}
else if(value.getClass().isPrimitive() || value instanceof String
|| value instanceof Tag || value instanceof Link
|| value instanceof Integer || value instanceof Long
|| value instanceof Float || value instanceof Double
|| value instanceof Boolean) {
if(append) {
concourse.add(key, value, id);
}
else {
concourse.set(key, value, id); // TODO use verifyOrSet when
// it becomes available
}
}
else if(value instanceof Enum) {
concourse.set(key, Tag.create(((Enum) value).name()), id);
}
else if(value instanceof Serializable) {
ByteBuffer bytes = Serializables.getBytes((Serializable) value);
Tag base64 = Tag.create(BaseEncoding.base64Url().encode(
ByteBuffers.toByteArray(bytes)));
store(key, base64, concourse, append);
}
else {
Gson gson = new Gson();
Tag json = Tag.create(gson.toJson(value));
store(key, json, concourse, append);
}
}
/**
* Returns a {@link JsonElement} representation of this record which
* includes all of its non private fields.
*
* @param seen - the records that have been previously serialized, so we
* don't recurse infinitely
*
* @return the JsonElement representation
*/
private JsonElement toJsonElement(Set seen) {
try {
Field[] fields = getAllDeclaredFields();
JsonObject json = new JsonObject();
json.addProperty("id", id);
Map more = getMoreData();
for (String key : more.keySet()) {
json.add(key, jsonify(more.get(key), seen));
}
for (Field field : fields) {
Object value = field.get(this);
if(!Modifier.isPrivate(field.getModifiers())
&& !Modifier.isTransient(field.getModifiers())
&& value != null) {
json.add(field.getName(), jsonify(value, seen));
}
}
return json;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
/**
* Returns a {@link JsonElement} representation of this record which
* includes all of the non private {@code keys} that are specified.
*
* @param keys
* @param seen - the records that have been previously serialized, so we
* don't recurse infinitely
* @return the JsonElement representation
*/
private JsonElement toJsonElement(Set seen, String... keys) {
try {
Set _keys = Sets.newHashSet(keys);
Field[] fields = getAllDeclaredFields();
JsonObject json = new JsonObject();
json.addProperty("id", id);
Map more = getMoreData();
for (String key : more.keySet()) {
if(_keys.contains(key)) {
json.add(key, jsonify(more.get(key), seen));
}
}
for (Field field : fields) {
Object value;
if(_keys.contains(field.getName())
&& !Modifier.isPrivate(field.getModifiers())
&& !Modifier.isTransient(field.getModifiers())
&& (value = field.get(this)) != null) {
json.add(field.getName(), jsonify(value, seen));
}
}
return json;
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
}