io.permazen.JTransaction Maven / Gradle / Ivy
Show all versions of permazen-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen;
import com.google.common.base.Converter;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Streams;
import com.google.common.reflect.TypeToken;
import io.permazen.core.CoreIndex;
import io.permazen.core.CoreIndex2;
import io.permazen.core.CoreIndex3;
import io.permazen.core.CoreIndex4;
import io.permazen.core.CounterField;
import io.permazen.core.CreateListener;
import io.permazen.core.DeleteListener;
import io.permazen.core.DeletedObjectException;
import io.permazen.core.Field;
import io.permazen.core.FieldSwitchAdapter;
import io.permazen.core.FieldType;
import io.permazen.core.ListField;
import io.permazen.core.MapField;
import io.permazen.core.ObjId;
import io.permazen.core.ObjType;
import io.permazen.core.ReferenceField;
import io.permazen.core.Schema;
import io.permazen.core.SetField;
import io.permazen.core.SimpleField;
import io.permazen.core.StaleTransactionException;
import io.permazen.core.Transaction;
import io.permazen.core.TypeNotInSchemaVersionException;
import io.permazen.core.UnknownFieldException;
import io.permazen.core.VersionChangeListener;
import io.permazen.core.type.ReferenceFieldType;
import io.permazen.core.util.ObjIdMap;
import io.permazen.core.util.ObjIdSet;
import io.permazen.index.Index;
import io.permazen.index.Index2;
import io.permazen.index.Index3;
import io.permazen.index.Index4;
import io.permazen.kv.KVDatabaseException;
import io.permazen.kv.KeyRanges;
import io.permazen.kv.util.AbstractKVNavigableSet;
import io.permazen.tuple.Tuple2;
import io.permazen.tuple.Tuple3;
import io.permazen.tuple.Tuple4;
import io.permazen.util.CloseableIterator;
import io.permazen.util.ConvertedNavigableMap;
import io.permazen.util.ConvertedNavigableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import org.dellroad.stuff.validation.ValidationContext;
import org.dellroad.stuff.validation.ValidationUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A transaction associated with a {@link Permazen} instance.
*
*
* Commonly used methods in this class can be divided into the following categories:
*
*
* Transaction Meta-Data
*
* - {@link #getPermazen getPermazen()} - Get the associated {@link Permazen} instance
* - {@link #getTransaction} - Get the core API {@link Transaction} underlying this instance
*
*
*
* Transaction Lifecycle
*
* - {@link #commit commit()} - Commit transaction
* - {@link #rollback rollback()} - Roll back transaction
* - {@link #getCurrent getCurrent()} - Get the {@link JTransaction} instance associated with the current thread
* - {@link #setCurrent setCurrent()} - Set the {@link JTransaction} instance associated with the current thread
* - {@link #isValid isValid()} - Test transaction validity
* - {@link #performAction performAction()} - Perform action with this instance as the current transaction
*
*
*
* Object Access
*
* - {@link #get(ObjId, Class) get()} - Get the Java model object corresponding to a specific database object ID
* - {@link #getAll getAll()} - Get all database objects that are instances of a given Java type
* - {@link #create(Class) create()} - Create a new database object
*
*
*
* Copying Objects
*
* - {@link #copyTo(JTransaction, CopyState, Stream) copyTo()}
* - Copy a {@link Stream} of objects into another transaction
* - {@link #copyTo(JTransaction, JObject, CopyState, String[]) copyTo()}
* - Copy objects reachable through specified reference path(s) into another transaction
* - {@link #cascadeFindAll(ObjId, String, int) cascadeFindAll()} - Find all objects reachable through a named cascade
* - {@link ImportContext} - Import plain (POJO) model objects
* - {@link ExportContext} - Export plain (POJO) model objects
*
*
*
* Validation
*
* - {@link #validate validate()} - Validate objects in the validation queue
* - {@link #resetValidationQueue} - Clear the validation queue
*
*
*
* Index Queries
*
* - {@link #queryIndex(Class, String, Class) queryIndex()}
* - Access the index associated with a simple field
* - {@link #queryListElementIndex queryListElementIndex()}
* - Access the composite index associated with a list field that includes corresponding list indicies
* - {@link #queryMapValueIndex queryMapValueIndex()}
* - Access the composite index associated with a map value field that includes corresponding map keys
* - {@link #queryCompositeIndex(Class, String, Class, Class) queryCompositeIndex()}
* - Access a composite index defined on two fields
* - {@link #queryCompositeIndex(Class, String, Class, Class, Class) queryCompositeIndex()}
* - Access a composite index defined on three fields
* - {@link #queryCompositeIndex(Class, String, Class, Class, Class, Class) queryCompositeIndex()}
* - Access a composite index defined on four fields
*
* - {@link #queryVersion queryVersion()} - Get database objects grouped according to their schema versions
*
*
*
* Reference Paths
*
* - {@link #followReferencePath followReferencePath()} - Find all objects referred to by to any element in a given set
* of starting objects through a specified {@link ReferencePath}
* - {@link #invertReferencePath invertReferencePath()} - Find all objects that refer to any element in a given set
* of objects through a specified {@link ReferencePath}
*
*
*
* Snapshot Transactions
*
* - {@link #getSnapshotTransaction getSnapshotTransaction()} - Get the default in-memory snapshot transaction
* associated with this regular transaction
* - {@link #createSnapshotTransaction createSnapshotTransaction()} - Create a new in-memory snapshot transaction
* - {@link #isSnapshot} - Determine whether this transaction is a snapshot transaction
*
*
*
* Lower Layer Access
*
* - {@link #getKey(JObject) getKey()} - Get the {@link io.permazen.kv.KVDatabase} key prefix for a specific object
* - {@link #getKey(JObject, String) getKey()} - Get the {@link io.permazen.kv.KVDatabase}
* key for a specific field in a specific object
*
*
*
* The remaining methods in this class are normally only used by generated Java model object subclasses.
* Instead of using these methods directly, using the appropriately annotated Java model object method
* or {@link JObject} interface method is recommended.
*
*
* Java Model Object Methods
*
* - {@link #readSimpleField readSimpleField()} - Read the value of a simple field
* - {@link #writeSimpleField writeSimpleField()} - Write the value of a simple field
* - {@link #readCounterField readCounterField()} - Access a {@link Counter} field
* - {@link #readSetField readSetField()} - Access a set field
* - {@link #readListField readListField()} - Access a list field
* - {@link #readMapField readMapField()} - Access a map field
* - {@link #registerJObject registerJObject()} - Ensure a {@link JObject} is registered in the object cache
*
*
*
* {@link JObject} Methods
*
* - {@link #delete delete()} - Delete an object from this transaction
* - {@link #exists exists()} - Test whether an object exists in this transaction
* - {@link #recreate recreate()} - Recreate an object in this transaction
* - {@link #revalidate revalidate()} - Manually add an object to the validation queue
* - {@link #getSchemaVersion getSchemaVersion()} - Get this schema version of an object
* - {@link #updateSchemaVersion updateSchemaVersion()} - Update an object's schema version
*
*/
@ThreadSafe
public class JTransaction {
private static final ThreadLocal CURRENT = new ThreadLocal<>();
private static final Class>[] DEFAULT_CLASS_ARRAY = { Default.class };
private static final Class>[] DEFAULT_AND_UNIQUENESS_CLASS_ARRAY = { Default.class, UniquenessConstraints.class };
private static final int MAX_UNIQUE_CONFLICTORS = 5;
final Logger log = LoggerFactory.getLogger(this.getClass());
final Permazen jdb;
final Transaction tx;
final ReferenceConverter referenceConverter = new ReferenceConverter<>(this, JObject.class);
private final ValidationMode validationMode;
@GuardedBy("this")
private final ObjIdMap[]> validationQueue = new ObjIdMap<>(); // maps object -> groups for pending validation
private final JObjectCache jobjectCache = new JObjectCache(this);
@GuardedBy("this")
private SnapshotJTransaction snapshotTransaction;
@GuardedBy("this")
private boolean commitInvoked;
// Constructor
/**
* Constructor.
*
* @throws IllegalArgumentException if any parameter is null
*/
JTransaction(Permazen jdb, Transaction tx, ValidationMode validationMode) {
// Initialization
Preconditions.checkArgument(jdb != null, "null jdb");
Preconditions.checkArgument(tx != null, "null tx");
Preconditions.checkArgument(validationMode != null, "null validationMode");
this.jdb = jdb;
this.tx = tx;
this.validationMode = validationMode;
// Set back-reference
tx.setUserObject(this);
// Register listeners, or re-use our existing listener set
final boolean automaticValidation = validationMode == ValidationMode.AUTOMATIC;
final boolean isSnapshot = this.isSnapshot();
final int listenerSetIndex = (automaticValidation ? 2 : 0) + (isSnapshot ? 0 : 1);
final Transaction.ListenerSet listenerSet = jdb.listenerSets[listenerSetIndex];
if (listenerSet == null) {
JTransaction.registerListeners(jdb, tx, automaticValidation, isSnapshot);
jdb.listenerSets[listenerSetIndex] = tx.snapshotListeners();
} else
tx.setListeners(listenerSet);
}
// Register listeners for the given situation
private static void registerListeners(Permazen jdb, Transaction tx, boolean automaticValidation, boolean isSnapshot) {
// Register listeners for @OnCreate and validation on creation
if (jdb.hasOnCreateMethods || (automaticValidation && jdb.anyJClassRequiresDefaultValidation))
tx.addCreateListener(new InternalCreateListener());
// Register listeners for @OnDelete
if (jdb.hasOnDeleteMethods)
tx.addDeleteListener(new InternalDeleteListener());
// Register listeners for @OnChange
for (JClass> jclass : jdb.jclasses.values()) {
for (OnChangeScanner>.MethodInfo info : jclass.onChangeMethods) {
if (isSnapshot && !info.getAnnotation().snapshotTransactions())
continue;
final OnChangeScanner>.ChangeMethodInfo changeInfo = (OnChangeScanner>.ChangeMethodInfo)info;
changeInfo.registerChangeListener(tx);
}
}
// Register field change listeners to trigger validation of corresponding JSR 303 and uniqueness constraints
if (automaticValidation) {
final DefaultValidationListener defaultValidationListener = new DefaultValidationListener();
jdb.fieldsRequiringDefaultValidation
.forEach(storageId -> tx.addFieldChangeListener(storageId, new int[0], null, defaultValidationListener));
}
// Register listeners for @OnVersionChange and validation on upgrade
if (jdb.hasOnVersionChangeMethods
|| jdb.hasUpgradeConversions
|| (automaticValidation && jdb.anyJClassRequiresDefaultValidation))
tx.addVersionChangeListener(new InternalVersionChangeListener());
}
// Thread-local Access
/**
* Get the {@link JTransaction} associated with the current thread, if any, otherwise throw an exception.
*
* @return instance previously associated with the current thread via {@link #setCurrent setCurrent()}
* @throws IllegalStateException if there is no such instance
*/
public static JTransaction getCurrent() {
final JTransaction jtx = CURRENT.get();
if (jtx == null) {
throw new IllegalStateException("there is no " + JTransaction.class.getSimpleName()
+ " associated with the current thread");
}
return jtx;
}
/**
* Set the {@link JTransaction} associated with the current thread.
*
* @param jtx transaction to associate with the current thread
*/
public static void setCurrent(JTransaction jtx) {
CURRENT.set(jtx);
}
// Accessors
/**
* Get the {@link Permazen} associated with this instance.
*
* @return the associated database
*/
public Permazen getPermazen() {
return this.jdb;
}
/**
* Get the {@link Transaction} associated with this instance.
*
* @return the associated core API transaction
*/
public Transaction getTransaction() {
return this.tx;
}
/**
* Get the {@link ValidationMode} configured for this instance.
*
* @return the configured validation mode
*/
public ValidationMode getValidationMode() {
return this.validationMode;
}
/**
* Get all instances of the given type.
*
*
* The returned set includes objects from all schema versions. Use {@link #queryVersion queryVersion()} to
* find objects with a specific schema version.
*
*
* The returned set is mutable, with the exception that {@link NavigableSet#add add()} is not supported.
* Deleting an element results in {@linkplain JObject#delete deleting} the corresponding object.
*
* @param type any Java type; use {@link Object Object.class} to return all database objects
* @param containing Java type
* @return a live view of all instances of {@code type}
* @throws IllegalArgumentException if {@code type} is null
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings("unchecked")
public NavigableSet getAll(Class type) {
Preconditions.checkArgument(type != null, "null type");
NavigableSet ids = this.tx.getAll();
final KeyRanges keyRanges = this.jdb.keyRangesFor(type);
if (!keyRanges.isFull())
ids = ((AbstractKVNavigableSet)ids).filterKeys(keyRanges);
return new ConvertedNavigableSet(ids, new ReferenceConverter(this, type));
}
/**
* Get all instances of the given type, grouped according to schema version.
*
* @param type any Java type; use {@link Object Object.class} to return all database objects
* @param containing Java type
* @return mapping from schema version to objects having that version
* @throws IllegalArgumentException if {@code type} is null
* @throws StaleTransactionException if this transaction is no longer usable
*/
public NavigableMap> queryVersion(Class type) {
Preconditions.checkArgument(type != null, "null type");
CoreIndex index = this.tx.queryVersion();
final KeyRanges keyRanges = this.jdb.keyRangesFor(type);
if (!keyRanges.isFull())
index = index.filter(1, keyRanges);
return new ConvertedNavigableMap, Integer, NavigableSet>(index.asMap(),
Converter.identity(), new NavigableSetConverter(new ReferenceConverter(this, type)));
}
/**
* Get the {@code byte[]} key in the underlying key/value store corresponding to the specified object.
*
*
* Notes:
*
* - Objects utilize mutiple keys; the return value is the common prefix of all such keys.
* - The {@link io.permazen.kv.KVDatabase} should not be modified directly, otherwise behavior is undefined
*
*
* @param jobj Java model object
* @return the {@link io.permazen.kv.KVDatabase} key corresponding to {@code jobj}
* @throws IllegalArgumentException if {@code jobj} is null
* @see io.permazen.kv.KVTransaction#watchKey KVTransaction.watchKey()
* @see io.permazen.core.Transaction#getKey(ObjId) Transaction.getKey()
*/
public byte[] getKey(JObject jobj) {
Preconditions.checkArgument(jobj != null, "null jobj");
return this.tx.getKey(jobj.getObjId());
}
/**
* Get the {@code byte[]} key in the underlying key/value store corresponding to the specified field in the specified object.
*
*
* Notes:
*
* - Complex fields utilize mutiple keys; the return value is the common prefix of all such keys.
* - The {@link io.permazen.kv.KVDatabase} should not be modified directly, otherwise behavior is undefined
*
*
* @param jobj Java model object
* @param fieldName the name of a field in {@code jobj}'s type
* @return the {@link io.permazen.kv.KVDatabase} key of the field in the specified object
* @throws TypeNotInSchemaVersionException if the current schema version does not contain the object's type
* @throws IllegalArgumentException if {@code jobj} does not contain the specified field
* @throws IllegalArgumentException if {@code fieldName} is otherwise invalid
* @throws IllegalArgumentException if either parameter is null
* @see io.permazen.kv.KVTransaction#watchKey KVTransaction.watchKey()
* @see io.permazen.core.Transaction#getKey(ObjId, int) Transaction.getKey()
*/
public byte[] getKey(JObject jobj, String fieldName) {
Preconditions.checkArgument(jobj != null, "null jobj");
final Class> type = this.jdb.getJClass(jobj.getObjId()).type;
final ReferencePath refPath = this.jdb.parseReferencePath(type, fieldName, true, false);
if (refPath.getReferenceFields().length > 0)
throw new IllegalArgumentException("invalid field name `" + fieldName + "'");
assert refPath.getTargetTypes().iterator().next().isInstance(jobj);
return this.tx.getKey(jobj.getObjId(), refPath.targetFieldStorageId);
}
// Snapshots
/**
* Determine whether this instance is a {@link SnapshotJTransaction}.
*
* @return true if this instance is a {@link SnapshotJTransaction}, otherwise false
*/
public boolean isSnapshot() {
return false;
}
/**
* Get the default {@link SnapshotJTransaction} associated with this instance.
*
*
* The default {@link SnapshotJTransaction} uses {@link ValidationMode#MANUAL}.
*
*
* This instance must not itself be a {@link SnapshotJTransaction}; use
* {@link createSnapshotTransaction createSnapshotTransaction()} to create additional snapshot transactions.
*
* @return the associated snapshot transaction
* @see JObject#copyOut JObject.copyOut()
* @throws IllegalArgumentException if this instance is itself a {@link SnapshotJTransaction}
*/
public synchronized SnapshotJTransaction getSnapshotTransaction() {
Preconditions.checkArgument(!this.isSnapshot(),
"getSnapshotTransaction() invoked on a snapshot transaction; use createSnapshotTransaction() instead");
if (this.snapshotTransaction == null)
this.snapshotTransaction = this.createSnapshotTransaction(ValidationMode.MANUAL);
return this.snapshotTransaction;
}
/**
* Create an empty snapshot transaction based on this instance.
*
*
* This new instance will have the same schema meta-data as this instance.
*
* @param validationMode the {@link ValidationMode} to use for the new transaction
* @return newly created snapshot transaction
* @throws IllegalArgumentException if {@code validationMode} is null
* @throws io.permazen.core.StaleTransactionException if this instance is no longer usable
*/
public SnapshotJTransaction createSnapshotTransaction(ValidationMode validationMode) {
return new SnapshotJTransaction(this.jdb, this.tx.createSnapshotTransaction(), validationMode);
}
/**
* Copy the specified object, and any other objects referenced through the specified reference paths,
* into the specified destination transaction.
*
*
* If the target object does not exist, it will be created, otherwise its schema version will be updated to match the source
* object if necessary (with resulting {@link io.permazen.annotation.OnVersionChange @OnVersionChange} notifications).
* If {@link CopyState#isSuppressNotifications()} returns false, {@link io.permazen.annotation.OnCreate @OnCreate}
* and {@link io.permazen.annotation.OnCreate @OnChange} notifications will also be delivered; however,
* these annotations must also have {@code snapshotTransactions = true} if {@code dest} is a {@link SnapshotJTransaction}).
*
*
* Circular references are handled properly: if an object is encountered more than once, it is not copied again.
* The {@code copyState} tracks which objects have already been copied and/or traversed along some reference path.
* For a "fresh" copy operation, pass a newly created {@link CopyState}; for a copy operation that is a continuation
* of a previous copy, reuse the previous {@code copyState}. The {@link CopyState} may also be configured to remap object ID's.
*
*
* This instance and {@code dest} must be compatible in that for any schema versions encountered, those schema versions
* must be identical in both transactions.
*
*
* If any copied objects contain reference fields configured with
* {@link io.permazen.annotation.JField#allowDeleted} equal to {@code false},
* then any objects referenced by those fields must also be copied, or else must already exist in {@code dest}
* (if {@code dest} is a {@link SnapshotJTransaction}, then {@link io.permazen.annotation.JField#allowDeletedSnapshot}
* applies instead). Otherwise, a {@link DeletedObjectException} is thrown and it is indeterminate which objects were copied.
*
*
* Note: if two threads attempt to copy objects between the same two transactions at the same time but in opposite directions,
* deadlock could result.
*
* @param dest destination transaction
* @param jobj object to copy
* @param copyState tracks which objects have already been copied and traversed and whether to remap object ID's
* @param refPaths zero or more reference paths that refer to additional objects to be copied (including intermediate objects)
* @return the copied object, i.e., the object having ID {@code dstId} in {@code dest}
* @throws DeletedObjectException if any object to be copied does not actually exist
* @throws DeletedObjectException if any copied object ends up with a reference to an object that does not exist
* in {@code dest} through a reference field configured to disallow deleted assignment
* @throws io.permazen.core.SchemaMismatchException
* if the schema corresponding to any copied object is not identical in both this instance and {@code dest}
* @throws TypeNotInSchemaVersionException if the current schema version does not contain the source object's type
* @throws StaleTransactionException if this transaction or {@code dest} is no longer usable
* @throws IllegalArgumentException if any path in {@code refPaths} is invalid
* @throws IllegalArgumentException if any parameter other than {@code dstId} is null
* @see JObject#copyTo JObject.copyTo()
* @see JObject#copyOut JObject.copyOut()
* @see JObject#copyIn JObject.copyIn()
* @see #copyTo(JTransaction, CopyState, Iterable)
* @see ReferencePath
*/
public JObject copyTo(JTransaction dest, JObject jobj, CopyState copyState, String... refPaths) {
// Sanity check
Preconditions.checkArgument(dest != null, "null dest");
Preconditions.checkArgument(jobj != null, "null jobj");
Preconditions.checkArgument(copyState != null, "null copyState");
Preconditions.checkArgument(refPaths != null, "null refPaths");
// Handle possible re-entrant object cache load
JTransaction.registerJObject(jobj);
// Get object ID
final ObjId id = jobj.getObjId();
// Parse paths and convert each into an array of reference fields (including the target reference field at the end)
final Class> startType = this.jdb.getJClass(id).type;
final ArrayList pathReferencesList = new ArrayList<>(refPaths.length);
for (String refPath : refPaths) {
// Parse reference path
Preconditions.checkArgument(refPath != null, "null refPath");
final ReferencePath path = this.jdb.parseReferencePath(startType, refPath, false, true);
// Append array of reference fields to our list
pathReferencesList.add(path.referenceFieldStorageIds);
}
// Reset deleted assignments
copyState.deletedAssignments.clear();
// Ensure object is copied even when there are zero reference paths
if (pathReferencesList.isEmpty())
this.copyTo(copyState, dest, id, true, 0, new int[0]);
// Recurse over each reference path
for (int[] pathReferences : pathReferencesList)
this.copyTo(copyState, dest, id, false/*doesn't matter*/, 0, pathReferences);
// Check for any remaining deleted assignments
copyState.checkDeletedAssignments(this);
// Done
return dest.get(copyState.getDestinationId(id));
}
/**
* Copy the specified objects into the specified destination transaction.
*
*
* This is a convenience method; equivalent to:
*
* {@link #copyTo(JTransaction, CopyState, Stream) copyTo}{@code
* (dest, copyState, }{@link Streams#stream(Iterable) Streams.stream}{@code (jobjs))}
*
*
* @param dest destination transaction
* @param copyState tracks which objects have already been copied and whether to remap object ID's
* @param jobjs {@link Iterable} returning the objects to copy; null values are ignored
* @throws DeletedObjectException if any object to be copied does not actually exist
* @throws DeletedObjectException if any copied object ends up with a reference to an object that does not exist
* in {@code dest} through a reference field configured to disallow deleted assignment
* @throws io.permazen.core.SchemaMismatchException
* if the schema corresponding to any copied object is not identical in both this instance and {@code dest}
* @throws StaleTransactionException if this transaction or {@code dest} is no longer usable
* @throws IllegalArgumentException if any parameter is null
*/
public void copyTo(JTransaction dest, CopyState copyState, Iterable extends JObject> jobjs) {
Preconditions.checkArgument(jobjs != null, "null jobjs");
this.copyTo(dest, copyState, Streams.stream(jobjs));
}
/**
* Copy the specified objects into the specified destination transaction.
*
*
* This is a convenience method; equivalent to {@link #copyTo(JTransaction, CopyState, Stream)}}
* but with the objects specified by object ID.
*
* @param dest destination transaction
* @param copyState tracks which objects have already been copied and whether to remap object ID's
* @param ids the object ID's of the objects to copy
* @throws DeletedObjectException if any object to be copied does not actually exist
* @throws DeletedObjectException if any copied object ends up with a reference to an object that does not exist
* in {@code dest} through a reference field configured to disallow deleted assignment
* @throws io.permazen.core.SchemaMismatchException
* if the schema corresponding to any copied object is not identical in both this instance and {@code dest}
* @throws StaleTransactionException if this transaction or {@code dest} is no longer usable
* @throws IllegalArgumentException if any parameter is null
*/
public void copyTo(JTransaction dest, CopyState copyState, ObjIdSet ids) {
Preconditions.checkArgument(ids != null, "null ids");
this.copyIdStreamTo(dest, copyState, ids.stream());
}
/**
* Copy the specified objects into the specified destination transaction.
*
*
* If a target object does not exist, it will be created, otherwise its schema version will be updated to match the source
* object if necessary (with resulting {@link io.permazen.annotation.OnVersionChange @OnVersionChange} notifications).
* If {@link CopyState#isSuppressNotifications()} returns false, {@link io.permazen.annotation.OnCreate @OnCreate}
* and {@link io.permazen.annotation.OnCreate @OnChange} notifications will also be delivered; however,
* these annotations must also have {@code snapshotTransactions = true} if {@code dest} is a {@link SnapshotJTransaction}).
*
*
* The {@code copyState} tracks which objects have already been copied. For a "fresh" copy operation,
* pass a newly created {@link CopyState}; for a copy operation that is a continuation of a previous copy,
* reuse the previous {@link CopyState}. The {@link CopyState} may also be configured to remap object ID's.
*
*
* This instance and {@code dest} must be compatible in that for any schema versions encountered, those schema versions
* must be identical in both transactions.
*
*
* If any copied objects contain reference fields configured with
* {@link io.permazen.annotation.JField#allowDeleted}{@code = false},
* then any objects referenced by those fields must also be copied, or else must already exist in {@code dest}
* (if {@code dest} is a {@link SnapshotJTransaction}, then {@link io.permazen.annotation.JField#allowDeletedSnapshot}
* applies instead). Otherwise, a {@link DeletedObjectException} is thrown and it is indeterminate which objects were copied.
*
*
* Note: if two threads attempt to copy objects between the same two transactions at the same time but in opposite directions,
* deadlock could result.
*
* @param dest destination transaction
* @param jobjs {@link Iterable} returning the objects to copy; null values are ignored
* @param copyState tracks which objects have already been copied and whether to remap object ID's
* @throws DeletedObjectException if any object to be copied does not actually exist
* @throws DeletedObjectException if any copied object ends up with a reference to an object that does not exist
* in {@code dest} through a reference field configured to disallow deleted assignment
* @throws io.permazen.core.SchemaMismatchException
* if the schema corresponding to any copied object is not identical in both this instance and {@code dest}
* @throws StaleTransactionException if this transaction or {@code dest} is no longer usable
* @throws IllegalArgumentException if {@code dest}, {@code copyState}, or {@code jobjs} is null
*/
public void copyTo(JTransaction dest, CopyState copyState, Stream extends JObject> jobjs) {
this.copyIdStreamTo(dest, copyState, jobjs
.filter(jobj -> jobj != null)
.peek(JTransaction::registerJObject) // handle possible re-entrant object cache load
.map(JObject::getObjId));
}
void copyIdStreamTo(JTransaction dest, CopyState copyState, Stream ids) {
// Sanity check
Preconditions.checkArgument(dest != null, "null dest");
Preconditions.checkArgument(copyState != null, "null copyState");
Preconditions.checkArgument(ids != null, "null ids");
// Reset deleted assignments
copyState.deletedAssignments.clear();
// Copy objects
ids.forEachOrdered(id -> this.copyTo(copyState, dest, id, true, 0, new int[0]));
// Check for any remaining deleted assignments
copyState.checkDeletedAssignments(this);
}
void copyTo(CopyState copyState, JTransaction dest, ObjId srcId, boolean required, int fieldIndex, int[] fields) {
// Copy current instance unless already copied, upgrading it in the process
if (copyState.markCopied(srcId)) {
// Get destination ID
final ObjId dstId = copyState.getDestinationId(srcId);
// See if we can disable listener notifications
boolean disableListenerNotifications = copyState.isSuppressNotifications();
final JClass> jclass = dest.jdb.jclasses.get(dstId.getStorageId());
if (!disableListenerNotifications && dest.isSnapshot() && jclass != null)
disableListenerNotifications = !jclass.hasSnapshotCreateOrChangeMethods;
// Reset any cached fields in the destination object
final JObject dstObject = dest.jobjectCache.getIfExists(dstId);
if (dstObject != null)
dstObject.resetCachedFieldValues();
// Copy object at the core API level
final ObjIdMap coreDeletedAssignments = new ObjIdMap<>();
boolean exists = true;
try {
this.tx.copy(srcId, dest.tx, true,
!disableListenerNotifications, coreDeletedAssignments, copyState.getObjectIdMap());
} catch (DeletedObjectException e) {
if (required)
throw e;
exists = false;
}
// Revalidate destination object if needed
if (dest.validationMode.equals(ValidationMode.AUTOMATIC) && jclass.requiresDefaultValidation)
dest.revalidate(Collections.singleton(dstId));
// Add any deleted assignments from the core API copy to our copy state
for (Map.Entry entry : coreDeletedAssignments.entrySet()) {
assert !copyState.isCopied(entry.getKey());
copyState.deletedAssignments.put(entry.getKey(), new DeletedAssignment(dstId, entry.getValue()));
}
// If the copy was successful, remove the copied object from the deleted assignments set in our copy state.
// This fixes up "forward reference" deleted assignments that get satisfied later in the overall copy operation.
if (exists)
copyState.deletedAssignments.remove(dstId);
}
// Any more fields to traverse?
if (fieldIndex == fields.length)
return;
// Have we already traversed the path?
final int[] pathSuffix = fieldIndex == 0 ? fields : Arrays.copyOfRange(fields, fieldIndex, fields.length);
if (!copyState.markTraversed(srcId, pathSuffix))
return;
// Recurse through the next reference field in the path
final int storageId = fields[fieldIndex++];
final SimpleFieldIndexInfo info = (SimpleFieldIndexInfo)this.jdb.indexInfoMap.get(storageId);
assert info == null || info.getFieldType() instanceof ReferenceFieldType;
if (info instanceof ComplexSubFieldIndexInfo) {
final ComplexSubFieldIndexInfo subFieldInfo = (ComplexSubFieldIndexInfo)info;
subFieldInfo.copyRecurse(copyState, this, dest, srcId, fieldIndex, fields);
} else {
final ObjId referrent = (ObjId)this.tx.readSimpleField(srcId, storageId, false);
if (referrent != null)
this.copyTo(copyState, dest, referrent, false, fieldIndex, fields);
}
}
/**
* Create a new object in this transaction of the same type for each object ID in the given set.
*
*
* The newly created objects will be in their initial states.
*
* @param ids object ID's
* @return mapping from object ID in {@code ids} to newly created object
* @throws IllegalArgumentException if {@code ids} is null
*/
public ObjIdMap createClones(ObjIdSet ids) {
Preconditions.checkArgument(ids != null, "null ids");
final ObjIdMap map = new ObjIdMap<>(ids.size());
for (ObjId id : ids)
map.put(id, this.tx.create(id.getStorageId()));
return map;
}
/**
* Recursively traverse cascade references starting from the given object to find all objects reachable
* through the specified cascade.
*
*
* This method finds all objects reachable from the starting object based on
* {@link io.permazen.annotation.JField#cascades @JField.cascades()} and
* {@link io.permazen.annotation.JField#inverseCascades @JField.inverseCascades()} annotation properties on
* reference fields: a reference field is traversed in the forward or inverse direction if {@code cascadeName} is
* specified in the corresponding annotation property. See {@link io.permazen.annotation.JField @JField} for details.
*
*
* All objects found will be {@linkplain #updateSchemaVersion upgraded} if necessary.
*
*
* The {@code recursionLimit} parameter can be used to limit the maximum distance of any reachable object,
* measured in the number of reference field "hops" from the given object.
*
* @param id starting object ID
* @param cascadeName cascade name, or null for no cascade (returns just the {@code id} object)
* @param recursionLimit the maximum number of references to hop through, or -1 for infinity
* @return the object ID's of all objects reachable through the specified cascade (including the {@code id} object)
* @throws DeletedObjectException if any object containing a traversed reference field does not actually exist
* @throws IllegalArgumentException if {@code recursionLimit} is less that -1
* @throws IllegalArgumentException if {@code id} is null
* @see JObject#cascadeCopyTo(JTransaction, String, int, boolean)
*/
public ObjIdSet cascadeFindAll(ObjId id, String cascadeName, int recursionLimit) {
final ObjIdSet ids = new ObjIdSet();
this.cascadeFindAll(id, cascadeName, recursionLimit, ids);
return ids;
}
/**
* Recursively traverse cascade references starting from the given object to find all objects reachable
* through the specified cascade.
*
*
* Upon invocation {@code visitedIds} contains the ID's of objects already visited; these objects will not be traversed.
* In particular, if {@code id} is in {@code visitedIds}, then this method does nothing.
* Upon return, {@code visitedIds} will also contain the ID's of all new objects found.
*
*
* All objects found will be {@linkplain #updateSchemaVersion upgraded} if necessary.
*
*
* The {@code recursionLimit} parameter can be used to limit the maximum distance of any reachable object,
* measured in the number of reference field "hops" from the given object.
*
* @param id starting object ID
* @param cascadeName cascade name, or null for no cascade
* @param recursionLimit the maximum number of references to hop through, or -1 for infinity
* @param visitedIds on entry objects already visited, on return all objects reachable
* @throws DeletedObjectException if any object containing a traversed reference field does not actually exist
* @throws IllegalArgumentException if {@code recursionLimit} is less that -1
* @throws IllegalArgumentException if {@code id} or {@code visitedIds} is null
* @see JObject#cascadeCopyTo(JTransaction, String, int, boolean)
*/
public void cascadeFindAll(ObjId id, String cascadeName, int recursionLimit, ObjIdSet visitedIds) {
// Sanity check
Preconditions.checkArgument(id != null, "null id");
Preconditions.checkArgument(recursionLimit >= -1, "recursionLimit < -1");
Preconditions.checkArgument(visitedIds != null, "null visitedIds");
// Initialize search
ObjIdSet toVisitIds = new ObjIdSet();
if (visitedIds.add(id))
toVisitIds.add(id);
// Do we need to cascade?
if (cascadeName == null)
return;
// While there are objects remaining to scan, cascade through forward and inverse reference field cascades
while (!toVisitIds.isEmpty()) {
assert visitedIds.containsAll(toVisitIds);
// Stop if we reach the recursion limit
if (recursionLimit != -1 && recursionLimit-- <= 0)
break;
// Find all new objects reachable in one hop from any object in 'toVisitIds'
final ObjIdSet newIds = new ObjIdSet();
for (ObjId toVisitId : toVisitIds) {
// Upgrade object if needed
this.tx.updateSchemaVersion(toVisitId);
// Gather references
this.gatherForwardCascadeRefs(toVisitId, cascadeName, visitedIds, newIds);
this.gatherInverseCascadeRefs(toVisitId, cascadeName, visitedIds, newIds);
}
// New objects will be expanded on next time
toVisitIds = newIds;
}
}
private void gatherForwardCascadeRefs(ObjId id, String cascadeName, ObjIdSet visitedIds, ObjIdSet toVisitIds) {
final JClass> jclass = this.jdb.jclasses.get(id.getStorageId());
if (jclass == null)
return;
final List fieldList = jclass.forwardCascadeMap.get(cascadeName);
if (fieldList == null)
return;
for (JReferenceField field : fieldList) {
final SimpleFieldIndexInfo info = (SimpleFieldIndexInfo)this.jdb.indexInfoMap.get(field.storageId);
assert info.getFieldType() instanceof ReferenceFieldType;
if (info instanceof ComplexSubFieldIndexInfo) {
final ComplexSubFieldIndexInfo subFieldInfo = (ComplexSubFieldIndexInfo)info;
this.gatherRefs(subFieldInfo.iterateReferences(this.tx, id).iterator(), visitedIds, toVisitIds);
} else {
final ObjId referrent = (ObjId)this.tx.readSimpleField(id, field.storageId, false);
if (referrent != null && visitedIds.add(referrent))
toVisitIds.add(referrent);
}
}
}
private void gatherInverseCascadeRefs(ObjId id, String cascadeName, ObjIdSet visitedIds, ObjIdSet toVisitIds) {
final List fieldList = this.jdb.inverseCascadeMap.get(cascadeName);
if (fieldList == null)
return;
for (JReferenceField field : fieldList) {
final JClass> jclass = field.getJClass();
// Access the index associated with the reference field's storage ID
CoreIndex, ?> index = this.tx.queryIndex(field.storageId);
// Restrict to references coming from objects having the type containing the particular reference field in question
index = index.filter(1, new KeyRanges(ObjId.getKeyRange(jclass.storageId)));
// Find objects of that type referring to "id" through the field and add them to our cascade
final NavigableSet> refs = index.asMap().get(id);
if (refs != null)
this.gatherRefs(refs.iterator(), visitedIds, toVisitIds);
}
}
private void gatherRefs(Iterator> i0, ObjIdSet visitedIds, ObjIdSet toVisitIds) {
try (CloseableIterator> i = CloseableIterator.wrap(i0)) {
while (i.hasNext()) {
final ObjId ref = (ObjId)i.next();
if (ref != null && visitedIds.add(ref))
toVisitIds.add(ref);
}
}
}
// Object/Field Access
/**
* Get the Java model object that is associated with this transaction and has the given ID.
*
*
* This method guarantees that for any particular {@code id}, the same Java instance will always be returned.
*
*
* A non-null object is always returned, but the corresponding object may not actually exist in this transaction.
* In that case, attempts to access its fields will throw {@link io.permazen.core.DeletedObjectException}.
* Use {@link JObject#exists JObject.exists()} to check.
*
*
* Also, it's possible that {@code id} corresponds to an object type that no longer exists in the schema
* version associated with this transaction. In that case, an {@link UntypedJObject} is returned.
*
* @param id object ID
* @return Java model object
* @throws IllegalArgumentException if {@code id} is null
* @see #get(ObjId, Class)
* @see #get(JObject)
*/
public JObject get(ObjId id) {
return this.jobjectCache.get(id);
}
/**
* Get the Java model object that is associated with this transaction and has the given ID, cast to the given type.
*
*
* This method guarantees that for any particular {@code id}, the same Java instance will always be returned.
*
*
* A non-null object is always returned, but the corresponding object may not actually exist in this transaction.
* In that case, attempts to access its fields will throw {@link io.permazen.core.DeletedObjectException}.
* Use {@link JObject#exists JObject.exists()} to check.
*
*
* This method just invoke {@link #get(ObjId)} and then casts the result.
*
* @param id object ID
* @param type expected type
* @param expected Java model type
* @return Java model object
* @throws ClassCastException if the Java model object does not have type {@code type}
* @throws IllegalArgumentException if {@code type} is null
* @see #get(ObjId)
* @see #get(JObject)
*/
public T get(ObjId id, Class type) {
Preconditions.checkArgument(type != null, "null type");
return type.cast(this.get(id));
}
/**
* Get the Java model object with the same object ID as the given {@link JObject} and whose state derives from this transaction.
*
*
* This method can be thought of as a "refresh" operation for objects being imported from other transactions into this one.
*
*
* A non-null object is always returned, but the corresponding object may not actually exist in this transaction.
* In that case, attempts to access its fields will throw {@link io.permazen.core.DeletedObjectException}.
* Use {@link JObject#exists JObject.exists()} to check.
*
*
* This method is equivalent to {@code get(jobj.getObjId())} followed by an appropriate cast to type {@code T}.
*
* @param jobj Java model object
* @param expected Java type
* @return Java model object in this transaction with the same object ID (possibly {@code jobj} itself)
* @throws IllegalArgumentException if {@code jobj} is null, or not a {@link Permazen} database object
* @throws ClassCastException if the Java model object in this transaction somehow does not have the same type as {@code jobj}
* @see #get(ObjId)
* @see #get(ObjId, Class)
*/
@SuppressWarnings("unchecked")
public T get(T jobj) {
return (T)jobj.getModelClass().cast(this.get(jobj.getObjId()));
}
/**
* Create a new instance of the given model class in this transaction.
*
* @param type Java object model type
* @param Java model type
* @return newly created instance
* @throws IllegalArgumentException if {@code type} is not a known Java object model type
* @throws StaleTransactionException if this transaction is no longer usable
*/
public T create(Class type) {
return this.create(this.jdb.getJClass(type));
}
/**
* Create a new instance of the given type in this transaction.
*
* @param jclass object type
* @param Java model type
* @return newly created instance
* @throws IllegalArgumentException if {@code jclass} is not valid for this instance
* @throws StaleTransactionException if this transaction is no longer usable
*/
public T create(JClass jclass) {
final ObjId id = this.tx.create(jclass.storageId);
return jclass.getType().cast(this.get(id));
}
/**
* Delete the object with the given object ID in this transaction.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#delete} would be used instead.
*
* @param jobj the object to delete
* @return true if object was found and deleted, false if object was not found
* @throws io.permazen.core.ReferencedObjectException if the object is referenced by some other object
* through a reference field configured for {@link io.permazen.core.DeleteAction#EXCEPTION}
* @throws StaleTransactionException if this transaction is no longer usable
* @throws NullPointerException if {@code jobj} is null
*/
public boolean delete(JObject jobj) {
// Handle possible re-entrant object cache load
JTransaction.registerJObject(jobj);
// Delete object
final ObjId id = jobj.getObjId();
final boolean deleted = this.tx.delete(id);
// Remove object from validation queue if enqueued
if (deleted) {
synchronized (this) {
this.validationQueue.remove(id);
}
}
// Reset cached field values
if (deleted)
jobj.resetCachedFieldValues();
// Done
return deleted;
}
/**
* Determine whether the object with the given object ID exists in this transaction.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#exists} would be used instead.
*
* @param id ID of the object to test for existence
* @return true if object was found, false if object was not found
* @throws StaleTransactionException if this transaction is no longer usable
* @throws NullPointerException if {@code id} is null
*/
public boolean exists(ObjId id) {
return this.tx.exists(id);
}
/**
* Recreate the given instance in this transaction.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#recreate} would be used instead.
*
* @param jobj the object to recreate
* @return true if the object was recreated, false if the object already existed
* @throws StaleTransactionException if this transaction is no longer usable
* @throws NullPointerException if {@code jobj} is null
*/
public boolean recreate(JObject jobj) {
JTransaction.registerJObject(jobj); // handle possible re-entrant object cache load
return this.tx.create(jobj.getObjId());
}
/**
* Add the given instance to the validation queue for validation, which will occur either at {@link #commit} time
* or at the next invocation of {@link #validate}, whichever occurs first.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#revalidate} would be used instead.
*
* @param id ID of the object to revalidate
* @param groups validation group(s) to use for validation; if empty, {@link javax.validation.groups.Default} is assumed
* @throws StaleTransactionException if this transaction is no longer usable
* @throws IllegalStateException if transaction commit is already in progress
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws IllegalArgumentException if either parameter is null
* @throws IllegalArgumentException if any group in {@code groups} is null
*/
public void revalidate(ObjId id, Class>... groups) {
if (!this.tx.exists(id))
throw new DeletedObjectException(this.tx, id);
this.revalidate(Collections.singleton(id), groups);
}
/**
* Clear the validation queue associated with this transaction. Any previously enqueued objects that have not
* yet been validated will no longer receive validation.
*
* @throws StaleTransactionException if this transaction is no longer usable
* @throws IllegalStateException if transaction commit is already in progress
*/
public synchronized void resetValidationQueue() {
if (!this.tx.isValid())
throw new StaleTransactionException(this.tx);
this.validationQueue.clear();
}
private synchronized void revalidate(Collection extends ObjId> ids, Class>... groups) {
// Sanity checks
if (!this.tx.isValid())
throw new StaleTransactionException(this.tx);
Preconditions.checkArgument(groups != null, "null groups");
for (Class> group : groups)
Preconditions.checkArgument(group != null, "null group");
if (this.validationMode == ValidationMode.DISABLED)
return;
// "Intern" default group array
if (groups.length == 0 || Arrays.equals(groups, DEFAULT_CLASS_ARRAY))
groups = DEFAULT_CLASS_ARRAY;
// Add to queue
for (ObjId id : ids) {
final Class>[] existingGroups = this.validationQueue.get(id);
if (existingGroups == null) {
this.validationQueue.put(id, groups);
continue;
}
if (existingGroups == groups) // i.e., both are DEFAULT_CLASS_ARRAY
continue;
final HashSet> newGroups = new HashSet<>(Arrays.asList(existingGroups));
newGroups.addAll(Arrays.asList(groups));
this.validationQueue.put(id, newGroups.toArray(new Class>[newGroups.size()]));
}
}
/**
* Get this schema version of the specified object. Does not change the object's schema version.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#getSchemaVersion} would be used instead.
*
* @param id ID of the object containing the field
* @return object's schema version
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws NullPointerException if {@code id} is null
*/
public int getSchemaVersion(ObjId id) {
return this.tx.getSchemaVersion(id);
}
/**
* Update the schema version of the specified object, if necessary, so that its version matches
* the schema version associated with this instance's {@link Permazen}.
*
*
* If a version change occurs, matching {@link io.permazen.annotation.OnVersionChange @OnVersionChange}
* methods will be invoked prior to this method returning.
*
*
* This method is typically only used by generated classes; normally, {@link JObject#upgrade} would be used instead.
*
* @param jobj object to update
* @return true if the object's schema version was changed, false if it was already updated
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if {@code jobj} does not exist in this transaction
* @throws TypeNotInSchemaVersionException if the current schema version does not contain the object's type
* @throws NullPointerException if {@code jobj} is null
*/
public boolean updateSchemaVersion(JObject jobj) {
JTransaction.registerJObject(jobj); // handle possible re-entrant object cache load
return this.tx.updateSchemaVersion(jobj.getObjId());
}
/**
* Ensure the given {@link JObject} is registered in its associated transaction's object cache.
*
*
* This method is used internally, to handle mutations in model class superclass constructors, which will occur
* before the newly created {@link JObject} is fully constructed and associated with its {@link JTransaction}.
*
* @param jobj object to register
* @throws NullPointerException if {@code jobj} is null
*/
public static void registerJObject(JObject jobj) {
jobj.getTransaction().jobjectCache.register(jobj);
}
/**
* Read a simple field. This returns the value returned by {@link Transaction#readSimpleField Transaction.readSimpleField()}
* with {@link ObjId}s converted into {@link JObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.JField @JField} getter override methods
* and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param storageId storage ID of the {@link JSimpleField}
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @return value of the field in the object
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws UnknownFieldException if no {@link JSimpleField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but the object has a type
* that does not exist in this instance's schema version
* @throws NullPointerException if {@code id} is null
*/
public Object readSimpleField(ObjId id, int storageId, boolean updateVersion) {
return this.convert(this.jdb.getJField(id, storageId, JSimpleField.class).getConverter(this),
this.tx.readSimpleField(id, storageId, updateVersion));
}
/**
* Write a simple field. This writes the value via {@link Transaction#writeSimpleField Transaction.writeSimpleField()}
* after converting {@link JObject}s into {@link ObjId}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.JField @JField} setter override methods
* and not normally invoked directly by user code.
*
* @param jobj object containing the field
* @param storageId storage ID of the {@link JSimpleField}
* @param value new value for the field
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if {@code jobj} does not exist in this transaction
* @throws UnknownFieldException if no {@link JSimpleField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but {@code jobj} has a type
* that does not exist in this instance's schema version
* @throws IllegalArgumentException if {@code value} is not an appropriate value for the field
* @throws NullPointerException if {@code jobj} is null
*/
public void writeSimpleField(JObject jobj, int storageId, Object value, boolean updateVersion) {
JTransaction.registerJObject(jobj); // handle possible re-entrant object cache load
final ObjId id = jobj.getObjId();
final Converter, ?> converter = this.jdb.getJField(id, storageId, JSimpleField.class).getConverter(this);
if (converter != null)
value = this.convert(converter.reverse(), value);
this.tx.writeSimpleField(id, storageId, value, updateVersion);
}
/**
* Read a counter field.
*
*
* This method is used by generated {@link io.permazen.annotation.JField @JField} getter override methods
* and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param storageId storage ID of the {@link JCounterField}
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @return value of the counter in the object
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws UnknownFieldException if no {@link JCounterField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but the object has a type
* that does not exist in this instance's schema version
* @throws NullPointerException if {@code id} is null
*/
public Counter readCounterField(ObjId id, int storageId, boolean updateVersion) {
this.jdb.getJField(id, storageId, JCounterField.class); // validate field type
if (updateVersion)
this.tx.updateSchemaVersion(id);
return new Counter(this.tx, id, storageId, updateVersion);
}
/**
* Read a set field. This returns the set returned by {@link Transaction#readSetField Transaction.readSetField()} with
* {@link ObjId}s converted into {@link JObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.JSetField @JSetField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param storageId storage ID of the {@link JSetField}
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @return the set field in the object with storage ID {@code storageId}
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws UnknownFieldException if no {@link JSetField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but the object has a type
* that does not exist in this instance's schema version
* @throws NullPointerException if {@code id} is null
*/
public NavigableSet> readSetField(ObjId id, int storageId, boolean updateVersion) {
return this.convert(this.jdb.getJField(id, storageId, JSetField.class).getConverter(this),
this.tx.readSetField(id, storageId, updateVersion));
}
/**
* Read a list field. This returns the list returned by {@link Transaction#readListField Transaction.readListField()} with
* {@link ObjId}s converted into {@link JObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.JListField @JListField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param storageId storage ID of the {@link JListField}
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @return the list field in the object with storage ID {@code storageId}
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws UnknownFieldException if no {@link JListField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but the object has a type
* that does not exist in this instance's schema version
* @throws NullPointerException if {@code id} is null
*/
public List> readListField(ObjId id, int storageId, boolean updateVersion) {
return this.convert(this.jdb.getJField(id, storageId, JListField.class).getConverter(this),
this.tx.readListField(id, storageId, updateVersion));
}
/**
* Read a map field. This returns the map returned by {@link Transaction#readMapField Transaction.readMapField()} with
* {@link ObjId}s converted into {@link JObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.JMapField @JMapField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param storageId storage ID of the {@link JMapField}
* @param updateVersion true to first automatically update the object's schema version, false to not change it
* @return the map field in the object with storage ID {@code storageId}
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if the object does not exist in this transaction
* @throws UnknownFieldException if no {@link JMapField} corresponding to {@code storageId} exists
* @throws TypeNotInSchemaVersionException if {@code updateVersion} is true but the object has a type
* that does not exist in this instance's schema version
* @throws NullPointerException if {@code id} is null
*/
public NavigableMap, ?> readMapField(ObjId id, int storageId, boolean updateVersion) {
return this.convert(this.jdb.getJField(id, storageId, JMapField.class).getConverter(this),
this.tx.readMapField(id, storageId, updateVersion));
}
// Reference Path Access
/**
* Find all objects that are reachable from the given starting object set through the specified {@link ReferencePath}.
*
* @param path reference path
* @param startObjects starting objects
* @return read-only set of objects reachable from {@code startObjects} via {@code path}
* @throws UnknownFieldException if {@code path} contains an unknown field
* @throws IllegalArgumentException if either parameter is null
* @see ReferencePath
*/
public NavigableSet followReferencePath(ReferencePath path, Iterable extends JObject> startObjects) {
Preconditions.checkArgument(path != null, "null path");
Preconditions.checkArgument(startObjects != null, "null startObjects");
final NavigableSet ids = this.tx.followReferencePath(Iterables.transform(startObjects, this.referenceConverter),
path.getReferenceFields(), path.getPathKeyRanges());
return new ConvertedNavigableSet(ids, this.referenceConverter);
}
/**
* Find all objects that refer to any object in the given target set through the specified {@link ReferencePath}.
*
* @param path reference path
* @param targetObjects target objects
* @return read-only set of objects that refer to any of the {@code targetObjects} via {@code path}
* @throws UnknownFieldException if {@code path} contains an unknown field
* @throws IllegalArgumentException if either parameter is null
* @see ReferencePath
*/
public NavigableSet invertReferencePath(ReferencePath path, Iterable extends JObject> targetObjects) {
Preconditions.checkArgument(path != null, "null path");
Preconditions.checkArgument(targetObjects != null, "null targetObjects");
final NavigableSet ids = this.tx.invertReferencePath(path.getReferenceFields(), path.getPathKeyRanges(),
Iterables.transform(targetObjects, this.referenceConverter));
return new ConvertedNavigableSet(ids, this.referenceConverter);
}
// Index Access
/**
* Get the index on a simple field. The simple field may be a sub-field of a complex field.
*
* @param targetType Java type containing the indexed field; may also be any super-type (e.g., an interface type),
* as long as {@code fieldName} is not ambiguous among all sub-types
* @param fieldName name of the indexed field; for complex fields,
* must include the sub-field name (e.g., {@code "mylist.element"}, {@code "mymap.key"})
* @param valueType the Java type corresponding to the field value
* @param Java type corresponding to the indexed field
* @param Java type containing the field
* @return read-only, real-time view of field values mapped to sets of objects having that value in the field
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index queryIndex(Class targetType, String fieldName, Class valueType) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(new IndexQueryInfoKey(fieldName, false, targetType, valueType));
assert info.indexInfo instanceof SimpleFieldIndexInfo; // otherwise getIndexQueryInfo() would have failed...
final SimpleFieldIndexInfo indexInfo = (SimpleFieldIndexInfo)info.indexInfo;
final CoreIndex, ObjId> index = info.applyFilters(this.tx.queryIndex(indexInfo.storageId));
final Converter, ?> valueConverter = indexInfo.getConverter(this).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex(index, valueConverter, targetConverter);
}
/**
* Get the composite index on a list field that includes list indicies.
*
* @param targetType type containing the indexed field; may also be any super-type (e.g., an interface type),
* as long as {@code fieldName} is not ambiguous among all sub-types
* @param fieldName name of the indexed field; must include {@code "element"} sub-field name (e.g., {@code "mylist.element"})
* @param valueType the Java type corresponding to list elements
* @param Java type corresponding to the indexed list's element field
* @param Java type containing the field
* @return read-only, real-time view of field values, objects having that value in the field, and corresponding list indicies
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index2 queryListElementIndex(Class targetType, String fieldName, Class valueType) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(new IndexQueryInfoKey(fieldName, false, targetType, valueType));
if (!(info.indexInfo instanceof ListElementIndexInfo))
throw new IllegalArgumentException("field `" + fieldName + "' is not a list element sub-field");
final ListElementIndexInfo indexInfo = (ListElementIndexInfo)info.indexInfo;
final CoreIndex2, ObjId, Integer> index = info.applyFilters(this.tx.queryListElementIndex(indexInfo.storageId));
final Converter, ?> valueConverter = indexInfo.getConverter(this).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex2(index, valueConverter, targetConverter, Converter.identity());
}
/**
* Get the composite index on a map value field that includes map keys.
*
* @param targetType type containing the indexed field; may also be any super-type (e.g., an interface type),
* as long as {@code fieldName} is not ambiguous among all sub-types
* @param fieldName name of the indexed field; must include {@code "value"} sub-field name (e.g., {@code "mymap.value"})
* @param valueType the Java type corresponding to map values
* @param keyType the Java type corresponding to map keys
* @param Java type corresponding to the indexed map's value field
* @param Java type containing the field
* @param Java type corresponding to the indexed map's key field
* @return read-only, real-time view of map values, objects having that value in the map field, and corresponding map keys
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index2 queryMapValueIndex(Class targetType,
String fieldName, Class valueType, Class keyType) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(
new IndexQueryInfoKey(fieldName, false, targetType, valueType, keyType));
if (!(info.indexInfo instanceof MapValueIndexInfo))
throw new IllegalArgumentException("field `" + fieldName + "' is not a map value sub-field");
final MapValueIndexInfo indexInfo = (MapValueIndexInfo)info.indexInfo;
final CoreIndex2, ObjId, ?> index = info.applyFilters(this.tx.queryMapValueIndex(indexInfo.storageId));
final Converter, ?> valueConverter = indexInfo.getConverter(this).reverse();
final Converter, ?> keyConverter = indexInfo.getKeyConverter(this).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex2(index, valueConverter, targetConverter, keyConverter);
}
/**
* Access a composite index on two fields.
*
* @param targetType type containing the indexed fields; may also be any super-type (e.g., an interface type)
* @param indexName the name of the composite index
* @param value1Type the Java type corresponding to the first field value
* @param value2Type the Java type corresponding to the second field value
* @param Java type corresponding to the first indexed field
* @param Java type corresponding to the second indexed field
* @param Java type containing the field
* @return read-only, real-time view of the fields' values and the objects having those values in the fields
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index2 queryCompositeIndex(Class targetType,
String indexName, Class value1Type, Class value2Type) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(
new IndexQueryInfoKey(indexName, true, targetType, value1Type, value2Type));
final CompositeIndexInfo indexInfo = (CompositeIndexInfo)info.indexInfo;
final CoreIndex2, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex2(indexInfo.storageId));
final Converter, ?> value1Converter = indexInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = indexInfo.getConverter(this, 1).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex2(index, value1Converter, value2Converter, targetConverter);
}
/**
* Access a composite index on three fields.
*
* @param targetType type containing the indexed fields; may also be any super-type (e.g., an interface type)
* @param indexName the name of the composite index
* @param value1Type the Java type corresponding to the first field value
* @param value2Type the Java type corresponding to the second field value
* @param value3Type the Java type corresponding to the third field value
* @param Java type corresponding to the first indexed field
* @param Java type corresponding to the second indexed field
* @param Java type corresponding to the third indexed field
* @param Java type containing the field
* @return read-only, real-time view of the fields' values and the objects having those values in the fields
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index3 queryCompositeIndex(Class targetType,
String indexName, Class value1Type, Class value2Type, Class value3Type) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(
new IndexQueryInfoKey(indexName, true, targetType, value1Type, value2Type, value3Type));
final CompositeIndexInfo indexInfo = (CompositeIndexInfo)info.indexInfo;
final CoreIndex3, ?, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex3(indexInfo.storageId));
final Converter, ?> value1Converter = indexInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = indexInfo.getConverter(this, 1).reverse();
final Converter, ?> value3Converter = indexInfo.getConverter(this, 2).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex3(index, value1Converter, value2Converter, value3Converter, targetConverter);
}
/**
* Access a composite index on four fields.
*
* @param targetType type containing the indexed fields; may also be any super-type (e.g., an interface type)
* @param indexName the name of the composite index
* @param value1Type the Java type corresponding to the first field value
* @param value2Type the Java type corresponding to the second field value
* @param value3Type the Java type corresponding to the third field value
* @param value4Type the Java type corresponding to the fourth field value
* @param Java type corresponding to the first indexed field
* @param Java type corresponding to the second indexed field
* @param Java type corresponding to the third indexed field
* @param Java type corresponding to the fourth indexed field
* @param Java type containing the field
* @return read-only, real-time view of the fields' values and the objects having those values in the fields
* @throws IllegalArgumentException if any parameter is null, or invalid
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Index4 queryCompositeIndex(Class targetType,
String indexName, Class value1Type, Class value2Type, Class value3Type, Class value4Type) {
final IndexQueryInfo info = this.jdb.getIndexQueryInfo(
new IndexQueryInfoKey(indexName, true, targetType, value1Type, value2Type, value3Type, value4Type));
final CompositeIndexInfo indexInfo = (CompositeIndexInfo)info.indexInfo;
final CoreIndex4, ?, ?, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex4(indexInfo.storageId));
final Converter, ?> value1Converter = indexInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = indexInfo.getConverter(this, 1).reverse();
final Converter, ?> value3Converter = indexInfo.getConverter(this, 2).reverse();
final Converter, ?> value4Converter = indexInfo.getConverter(this, 3).reverse();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex4(index, value1Converter, value2Converter, value3Converter, value4Converter, targetConverter);
}
// COMPOSITE-INDEX
/**
* Query an index by storage ID. For storage ID's corresponding to simple fields, this method returns an
* {@link Index}, except for list element and map value fields, for which an {@link Index2} is returned.
* For storage ID's corresponding to composite indexes, this method returns an {@link Index2}, {@link Index3},
* etc. as appropriate.
*
*
* This method exists mainly for the convenience of programmatic tools, etc.
*
* @param storageId indexed {@link JSimpleField}'s storage ID
* @return read-only, real-time view of the fields' values and the objects having those values in the fields
* @throws IllegalArgumentException if {@code storageId} does not correspond to an indexed field or composite index
* @throws StaleTransactionException if this transaction is no longer usable
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Object queryIndex(int storageId) {
// Find index
final IndexInfo indexInfo = this.jdb.indexInfoMap.get(storageId);
if (indexInfo == null)
throw new IllegalArgumentException("no composite index or simple indexed field exists with storage ID " + storageId);
// Handle a composite index
if (indexInfo instanceof CompositeIndexInfo) {
final CompositeIndexInfo compositeInfo = (CompositeIndexInfo)indexInfo;
switch (compositeInfo.getStorageIds().size()) {
case 2:
{
final Converter, ?> value1Converter = compositeInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = compositeInfo.getConverter(this, 1).reverse();
return new ConvertedIndex2(this.tx.queryCompositeIndex2(indexInfo.storageId),
value1Converter, value2Converter, this.referenceConverter);
}
case 3:
{
final Converter, ?> value1Converter = compositeInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = compositeInfo.getConverter(this, 1).reverse();
final Converter, ?> value3Converter = compositeInfo.getConverter(this, 2).reverse();
return new ConvertedIndex3(this.tx.queryCompositeIndex3(indexInfo.storageId),
value1Converter, value2Converter, value3Converter, this.referenceConverter);
}
case 4:
{
final Converter, ?> value1Converter = compositeInfo.getConverter(this, 0).reverse();
final Converter, ?> value2Converter = compositeInfo.getConverter(this, 1).reverse();
final Converter, ?> value3Converter = compositeInfo.getConverter(this, 2).reverse();
final Converter, ?> value4Converter = compositeInfo.getConverter(this, 3).reverse();
return new ConvertedIndex4(this.tx.queryCompositeIndex4(indexInfo.storageId),
value1Converter, value2Converter, value3Converter, value4Converter, this.referenceConverter);
}
// COMPOSITE-INDEX
default:
throw new RuntimeException("internal error");
}
}
// Must be a simple field index
return ((SimpleFieldIndexInfo)indexInfo).toIndex(this);
}
// Transaction Lifecycle
/**
* Commit this transaction.
*
*
* Prior to actual commit, if this transaction was created with a validation mode other than {@link ValidationMode#DISABLED},
* {@linkplain #validate validation} of outstanding objects in the validation queue is performed.
*
*
* If a {@link ValidationException} is thrown, the transaction is no longer usable. To perform validation and leave
* the transaction open, invoke {@link #validate} prior to commit.
*
* @throws StaleTransactionException if this transaction is no longer usable
* @throws io.permazen.kv.RetryTransactionException from {@link io.permazen.kv.KVTransaction#commit KVTransaction.commit()}
* @throws ValidationException if a validation error is detected
* @throws IllegalStateException if this method is invoked re-entrantly from within a validation check
*/
public synchronized void commit() {
// Sanity check
if (!this.tx.isValid())
throw new StaleTransactionException(this.tx);
synchronized (this) {
if (this.commitInvoked)
throw new IllegalStateException("commit() invoked re-entrantly");
this.commitInvoked = true;
}
// Do validation
try {
this.validate();
} catch (ValidationException e) {
this.tx.rollback();
throw e;
}
// Commit
this.tx.commit();
}
/**
* Roll back this transaction.
*
*
* This method may be invoked at any time, even after a previous invocation of
* {@link #commit} or {@link #rollback}, in which case the invocation will be ignored.
*/
public void rollback() {
this.tx.rollback();
}
/**
* Determine whether this transaction is still usable.
*
* @return true if this transaction is still valid
* @see Transaction#isValid
*/
public boolean isValid() {
return this.tx.isValid();
}
/**
* Perform validation checks on all objects currently in the validation queue.
* This method may be called at any time prior to {@link #commit} to
* process and clear the queue of validatable objects.
*
*
* If validation fails, validation stops, all remaining unvalidated objects are left on the validation queue,
* and a {@link ValidationException} is thrown. The transaction will remain usable.
*
*
* Note: if the this transaction was created with {@link ValidationMode#DISABLED}, then this method does nothing.
*
* @throws io.permazen.kv.RetryTransactionException from {@link io.permazen.kv.KVTransaction#commit KVTransaction.commit()}
* @throws ValidationException if a validation error is detected
* @throws IllegalStateException if transaction commit is already in progress
* @throws StaleTransactionException if this transaction is no longer usable
*
* @see JObject#revalidate
*/
public void validate() {
// Sanity check
if (!this.tx.isValid())
throw new StaleTransactionException(this.tx);
// Check validation mode
if (this.validationMode == ValidationMode.DISABLED)
return;
// Do validation
this.performAction(this::doValidate);
}
/**
* Invoke the given {@link Runnable} with this instance as the {@linkplain #getCurrent current transaction}.
*
*
* If another instance is currently associated with the current thread, it is set aside for the duration of
* {@code action}'s execution, and then restored when {@code action} is finished (regardless of outcome).
*
* @param action action to perform
* @throws IllegalArgumentException if {@code action} is null
*/
public void performAction(Runnable action) {
Preconditions.checkArgument(action != null, "null action");
final JTransaction previous = CURRENT.get();
CURRENT.set(this);
try {
action.run();
} finally {
CURRENT.set(previous);
}
}
// Internal methods
@SuppressWarnings("unchecked")
private void doValidate() {
final ValidatorFactory validatorFactory = this.jdb.getValidatorFactory();
final Validator validator = validatorFactory != null ? validatorFactory.getValidator() : null;
while (true) {
// Pop next object to validate off the queue
final ObjId id;
final Class>[] validationGroups;
synchronized (this) {
final Map.Entry[]> entry = this.validationQueue.removeOne();
if (entry == null)
return;
id = entry.getKey();
validationGroups = entry.getValue();
assert id != null;
assert validationGroups != null;
}
// Does it still exist?
if (!this.tx.exists(id))
continue;
// Get object and verify type exists in current schema (if not, the remaining validation is unneccessary)
final JObject jobj = this.get(id);
final JClass> jclass = this.jdb.jclasses.get(id.getStorageId());
if (jclass == null)
return;
// Do JSR 303 validation if needed
if (validator != null) {
final Set> violations;
try {
violations = new ValidationContext(jobj, validationGroups).validate(validator);
} catch (RuntimeException e) {
final Throwable rootCause = Throwables.getRootCause(e);
if (rootCause instanceof KVDatabaseException)
throw (KVDatabaseException)rootCause;
throw e;
}
if (!violations.isEmpty()) {
throw new ValidationException(jobj, violations, "validation error for object " + id + " of type `"
+ this.jdb.jclasses.get(id.getStorageId()).name + "':\n" + ValidationUtil.describe(violations));
}
}
// Do @OnValidate method validation
for (OnValidateScanner>.MethodInfo info : jclass.onValidateMethods) {
Class>[] methodGroups = info.getAnnotation().groups();
if (methodGroups.length == 0)
methodGroups = DEFAULT_CLASS_ARRAY;
if (Util.isAnyGroupBeingValidated(methodGroups, validationGroups))
Util.invoke(info.getMethod(), jobj);
}
// Do uniqueness validation
if ((!jclass.uniqueConstraintFields.isEmpty() || !jclass.uniqueConstraintCompositeIndexes.isEmpty())
&& Util.isAnyGroupBeingValidated(DEFAULT_AND_UNIQUENESS_CLASS_ARRAY, validationGroups)) {
// Check simple index uniqueness constraints
for (JSimpleField jfield : jclass.uniqueConstraintFields) {
assert jfield.indexed;
assert jfield.unique;
// Get field's (core API) value
final Object value = this.tx.readSimpleField(id, jfield.storageId, false);
// Compare to excluded value list
if (jfield.uniqueExcludes != null
&& Collections.binarySearch(jfield.uniqueExcludes, value, (Comparator