io.permazen.PermazenTransaction 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.AbstractIterator;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import io.permazen.annotation.OnChange;
import io.permazen.annotation.OnCreate;
import io.permazen.annotation.OnSchemaChange;
import io.permazen.annotation.PermazenType;
import io.permazen.core.CoreIndex1;
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.DeleteAction;
import io.permazen.core.DeletedObjectException;
import io.permazen.core.EnumValue;
import io.permazen.core.Field;
import io.permazen.core.FieldSwitch;
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.SchemaChangeListener;
import io.permazen.core.SchemaMismatchException;
import io.permazen.core.SetField;
import io.permazen.core.SimpleField;
import io.permazen.core.StaleTransactionException;
import io.permazen.core.Transaction;
import io.permazen.core.TypeNotInSchemaException;
import io.permazen.core.UnknownFieldException;
import io.permazen.core.UnknownTypeException;
import io.permazen.core.util.ObjIdMap;
import io.permazen.core.util.ObjIdSet;
import io.permazen.encoding.Encoding;
import io.permazen.index.Index;
import io.permazen.index.Index1;
import io.permazen.index.Index2;
import io.permazen.index.Index3;
import io.permazen.index.Index4;
import io.permazen.kv.KVDatabase;
import io.permazen.kv.KVDatabaseException;
import io.permazen.kv.KVTransaction;
import io.permazen.kv.KeyRanges;
import io.permazen.kv.util.AbstractKVNavigableSet;
import io.permazen.schema.SchemaId;
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 jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.groups.Default;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
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.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
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 PermazenTransaction} instance associated with the current thread
* - {@link #setCurrent setCurrent()} - Set the {@link PermazenTransaction} instance associated with the current thread
* - {@link #isOpen isOpen()} - Test whether transaction is still open
* - {@link #performAction performAction()} - Perform action with this instance as the current transaction
*
*
*
* Object Access
*
* - {@link #get(ObjId, Class) get()} - Get the Java model object in this transaction corresponding to a
* specific database object ID
* - {@link #getSingleton(Class) getSingleton()} - Get a singleton instance in this transaction, creating it on demand
* if needed
* - {@link #getAll getAll()} - Get all Java model objects in this transaction that are instances of a given Java type
* - {@link #create(Class) create()} - Create a new Java model object in this transaction
* - {@link #cascade cascade()} - Find all objects reachable through a reference cascade
*
*
*
* Copying Objects
*
* - {@link #copyTo(PermazenTransaction, CopyState, Stream) copyTo()} - Copy a {@link Stream} of objects into another
* transaction
* - {@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 #querySimpleIndex(Class, String, Class) querySimpleIndex()}
* - Access the index associated with a simple field
* - {@link #queryListElementIndex queryListElementIndex()}
* - Access the composite index associated with a list field that includes corresponding list indices
* - {@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 #querySchemaIndex querySchemaIndex()} - Get database objects grouped by schema
*
*
*
* Reference Paths
*
* - {@link #followReferencePath followReferencePath()} - Find all objects reachable by traversing a {@link ReferencePath}
* - {@link #invertReferencePath invertReferencePath()} - Find all objects reachable by traversing a {@link ReferencePath}
* in the reverse direction
*
*
*
* Detached Transactions
*
* - {@link #getDetachedTransaction getDetachedTransaction()} - Get the default in-memory detached transaction
* associated with this regular transaction
* - {@link #createDetachedTransaction createDetachedTransaction()} - Create a new in-memory detached transaction
* - {@link #createSnapshotTransaction createSnapshotTransaction()} - Create a new in-memory detached transaction
* pre-populated with a snapshot of this transaction
* - {@link #isDetached} - Determine whether this transaction is a detached transaction
*
*
*
* Lower Layer Access
*
* - {@link #getKey(PermazenObject) getKey()} - Get the {@link KVDatabase} key prefix for an object
* - {@link PermazenField#getKey(PermazenObject) PermazenField.getKey()} - Get the {@link KVDatabase} key for one field
* in an object
* - {@link #withWeakConsistency withWeakConsistency()} - Perform an operation with weaker transaction consistency
*
*
*
* 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 PermazenObject} 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 #registerPermazenObject registerPermazenObject()} - Ensure a {@link PermazenObject} is registered
* in the internal object cache
*
*
*
* {@link PermazenObject} 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 #migrateSchema migrateSchema()} - Migrate an object's schema to match this transaction's data model
*
*/
@ThreadSafe
public class PermazenTransaction {
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 pdb;
final Transaction tx;
final ReferenceConverter referenceConverter = new ReferenceConverter<>(this, PermazenObject.class);
private final ValidationMode validationMode;
@GuardedBy("this")
private final ObjIdMap[]> validationQueue = new ObjIdMap<>(); // maps object -> groups for pending validation
private final PermazenObjectCache pobjectCache = new PermazenObjectCache(this);
@GuardedBy("this")
private DetachedPermazenTransaction detachedTransaction;
@GuardedBy("this")
private boolean commitInvoked;
// Constructor
/**
* Constructor.
*
* @throws IllegalArgumentException if any parameter is null
*/
PermazenTransaction(Permazen pdb, Transaction tx, ValidationMode validationMode) {
// Initialization
Preconditions.checkArgument(pdb != null, "null pdb");
Preconditions.checkArgument(tx != null, "null tx");
Preconditions.checkArgument(validationMode != null, "null validationMode");
this.pdb = pdb;
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 isDetached = this.isDetached();
final int listenerSetIndex = (automaticValidation ? 2 : 0) + (isDetached ? 0 : 1);
final Transaction.ListenerSet listenerSet = pdb.listenerSets[listenerSetIndex];
if (listenerSet == null) {
PermazenTransaction.registerListeners(pdb, tx, automaticValidation, isDetached);
pdb.listenerSets[listenerSetIndex] = tx.snapshotListeners();
} else
tx.setListeners(listenerSet);
}
// Register listeners for the given situation
private static void registerListeners(Permazen pdb, Transaction tx, boolean automaticValidation, boolean isDetached) {
// Register create listeners for @OnCreate
for (PermazenClass> pclass : pdb.pclasses) {
for (OnCreateScanner>.MethodInfo info : pclass.onCreateMethods) {
final OnCreateScanner>.CreateMethodInfo createInfo = (OnCreateScanner>.CreateMethodInfo)info;
createInfo.registerCreateListeners(tx);
}
}
// Register create listeners for automatic validation on creation
for (PermazenClass> pclass : pdb.pclasses) {
if (automaticValidation && pclass.requiresDefaultValidation)
tx.addCreateListener(pclass.storageId, new ValidateOnCreateListener());
}
// Register delete listeners for @OnDelete
for (PermazenClass> pclass : pdb.pclasses) {
for (OnDeleteScanner>.MethodInfo info : pclass.onDeleteMethods) {
final OnDeleteScanner>.DeleteMethodInfo deleteInfo = (OnDeleteScanner>.DeleteMethodInfo)info;
deleteInfo.registerDeleteListener(tx);
}
}
// Register field change listeners for @OnChange
for (PermazenClass> pclass : pdb.pclasses) {
for (OnChangeScanner>.MethodInfo info : pclass.onChangeMethods) {
final OnChangeScanner>.ChangeMethodInfo changeInfo = (OnChangeScanner>.ChangeMethodInfo)info;
changeInfo.registerChangeListener(tx);
}
}
// Register field change listeners for automatic validation of JSR 303 and uniqueness constraints
if (automaticValidation) {
final DefaultValidationListener defaultValidationListener = new DefaultValidationListener();
pdb.fieldsRequiringDefaultValidation
.forEach(storageId -> tx.addFieldChangeListener(storageId, new int[0], null, defaultValidationListener));
}
// Register schema change listeners for @OnSchemaChange and automatic field conversion and/or validation on upgrade
for (PermazenClass> pclass : pdb.pclasses) {
if (!pclass.onSchemaChangeMethods.isEmpty()
|| !pclass.upgradeConversionFields.isEmpty()
|| (automaticValidation && pclass.requiresDefaultValidation))
tx.addSchemaChangeListener(pclass.storageId, new InternalSchemaChangeListener());
}
}
// Thread-local Access
/**
* Get the {@link PermazenTransaction} 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 PermazenTransaction getCurrent() {
final PermazenTransaction ptx = CURRENT.get();
if (ptx == null) {
throw new IllegalStateException(String.format(
"there is no %s associated with the current thread", PermazenTransaction.class.getSimpleName()));
}
return ptx;
}
/**
* Set the {@link PermazenTransaction} associated with the current thread.
*
* @param ptx transaction to associate with the current thread
*/
public static void setCurrent(PermazenTransaction ptx) {
CURRENT.set(ptx);
}
/**
* Determine if there is a {@link PermazenTransaction} associated with the current thread.
*
* @return true if there is a current {@link PermazenTransaction}, otherwise false
*/
public static boolean hasCurrent() {
return CURRENT.get() != null;
}
// Accessors
/**
* Get the {@link Permazen} associated with this instance.
*
* @return the associated database
*/
public Permazen getPermazen() {
return this.pdb;
}
/**
* 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 schemas. Use {@link #querySchemaIndex querySchemaIndex()} to
* find objects with a specific schema.
*
*
* The returned set is mutable, with the exception that {@link NavigableSet#add add()} is not supported.
* Deleting an element results in {@linkplain PermazenObject#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.pdb.keyRangesFor(type);
if (!keyRanges.isFull())
ids = ((AbstractKVNavigableSet)ids).filterKeys(keyRanges);
return new ConvertedNavigableSet(ids, new ReferenceConverter(this, type));
}
/**
* Get the singleton instance of the given type, creating it on demand if needed.
*
*
* This is a convenience method for accessing the singleton instances of any type annotated with
* {@link PermazenType @PermazenType}{@code (singleton = true)}. If {@code type} is not a
* singleton type, an exception is thrown.
*
*
* If no instance exists yet, one is created. So this method is essentially shorthand for:
*
*
* try {
* return this.getAll(type).first();
* } catch (NoSuchElementException e) {
* return this.create(type);
* }
*
*
* @param type singleton model class
* @param singleton type
* @return the singleton instance of type {@code type}
* @throws IllegalArgumentException if {@code type} is not a singleton type
* @throws IllegalArgumentException if {@code type} is null
* @throws StaleTransactionException if this transaction is no longer usable
* @see PermazenType#singleton
*/
@SuppressWarnings("unchecked")
public T getSingleton(Class type) {
Preconditions.checkArgument(type != null, "null type");
final PermazenClass pclass = this.pdb.getPermazenClass(type);
if (!pclass.singleton)
throw new IllegalArgumentException(String.format("model type is not a singleton type: %s", type));
final AbstractKVNavigableSet ids = (AbstractKVNavigableSet)this.tx.getAll(pclass.name);
final ObjId id;
try {
id = ids.first();
} catch (NoSuchElementException e) {
return this.create(type);
}
return type.cast(this.get(id));
}
/**
* Get all instances of the given type, grouped according to schema index.
*
* @param type any Java type; use {@link Object Object.class} to return all database objects
* @param containing Java type
* @return live, read-only mapping from {@link SchemaId} to objects having that schema
* @throws IllegalArgumentException if {@code type} is null
* @throws StaleTransactionException if this transaction is no longer usable
*/
public NavigableMap> querySchemaIndex(Class type) {
Preconditions.checkArgument(type != null, "null type");
CoreIndex1 index = this.tx.querySchemaIndex();
final KeyRanges keyRanges = this.pdb.keyRangesFor(type);
if (!keyRanges.isFull())
index = index.filter(1, keyRanges);
final Converter schemaIndexConverter = Converter.from(
schemaId -> this.tx.getSchemaBundle().getSchema(schemaId).getSchemaIndex(),
schemaIndex -> this.tx.getSchemaBundle().getSchema(schemaIndex).getSchemaId());
final Converter, NavigableSet> valueConverter
= new NavigableSetConverter(new ReferenceConverter(this, type));
return new ConvertedNavigableMap<>(index.asMap(), schemaIndexConverter, valueConverter);
}
/**
* Get the {@code byte[]} key in the underlying key/value store corresponding to the specified object.
*
*
* Notes:
*
* - Objects utilize multiple keys; the return value is the common prefix of all such keys.
* - The {@link KVDatabase} should not be modified directly, otherwise behavior is undefined
*
*
* @param pobj Java model object
* @return the {@link KVDatabase} key corresponding to {@code pobj}
* @throws IllegalArgumentException if {@code pobj} is null
* @see KVTransaction#watchKey KVTransaction.watchKey()
* @see Transaction#getKey(ObjId) Transaction.getKey()
*/
public byte[] getKey(PermazenObject pobj) {
Preconditions.checkArgument(pobj != null, "null pobj");
return this.tx.getKey(pobj.getObjId());
}
// Detached transactions
/**
* Determine whether this instance is a {@link DetachedPermazenTransaction}.
*
* @return true if this instance is a {@link DetachedPermazenTransaction}, otherwise false
*/
public boolean isDetached() {
return false;
}
/**
* Get the default {@link DetachedPermazenTransaction} associated with this instance.
*
*
* The default {@link DetachedPermazenTransaction} uses {@link ValidationMode#MANUAL}.
*
*
* This instance must not itself be a {@link DetachedPermazenTransaction}; use
* {@link #createDetachedTransaction createDetachedTransaction()} to create additional detached transactions.
*
* @return the associated detached transaction
* @see PermazenObject#copyOut PermazenObject.copyOut()
* @throws IllegalArgumentException if this instance is itself a {@link DetachedPermazenTransaction}
*/
public synchronized DetachedPermazenTransaction getDetachedTransaction() {
Preconditions.checkArgument(!this.isDetached(),
"getDetachedTransaction() invoked on a detached transaction; use createDetachedTransaction() instead");
if (this.detachedTransaction == null)
this.detachedTransaction = this.createDetachedTransaction(ValidationMode.MANUAL);
return this.detachedTransaction;
}
/**
* Create a new, empty detached transaction.
*
*
* The returned transaction will have the same schema meta-data as this instance.
* It will be a mutable transaction, but being detached, changes can't be committed.
*
*
* The returned {@link DetachedPermazenTransaction} does not support {@link #commit} or {@link #rollback}.
* It can be used indefinitely after this transaction closes, but it must be
* {@link DetachedPermazenTransaction#close close()}'d when no longer needed to release any associated resources.
*
* @param validationMode the {@link ValidationMode} to use for the new transaction
* @return newly created detached transaction
* @throws IllegalArgumentException if {@code validationMode} is null
* @throws io.permazen.core.StaleTransactionException if this instance is no longer usable
*/
public DetachedPermazenTransaction createDetachedTransaction(ValidationMode validationMode) {
return new DetachedPermazenTransaction(this.pdb, this.tx.createDetachedTransaction(), validationMode);
}
/**
* Create a new detached transaction pre-populated with a snapshot of this transaction.
*
*
* The returned transaction will have the same schema meta-data and object content as this instance.
* It will be a mutable transaction, but being detached, changes can't be committed.
*
*
* The returned {@link DetachedPermazenTransaction} does not support {@link #commit} or {@link #rollback}.
* It can be used indefinitely after this transaction closes, but it must be
* {@link DetachedPermazenTransaction#close close()}'d when no longer needed to release any associated resources.
*
*
* This method requires the underlying key/value transaction to support {@link KVTransaction#readOnlySnapshot}.
* As with any other information extracted from this transaction, the returned content is not guaranteed to be
* valid until this transaction has been successfully committed.
*
* @param validationMode the {@link ValidationMode} to use for the new transaction
* @return newly created detached transaction
* @throws IllegalArgumentException if {@code validationMode} is null
* @throws UnsupportedOperationException if they underlying key/value transaction doesn't support
* {@link KVTransaction#readOnlySnapshot}
* @throws io.permazen.core.StaleTransactionException if this instance is no longer usable
*/
public DetachedPermazenTransaction createSnapshotTransaction(ValidationMode validationMode) {
return new DetachedPermazenTransaction(this.pdb, this.tx.createSnapshotTransaction(), validationMode);
}
/**
* Copy the specified objects into the specified destination transaction.
*
*
* This is a convenience method; equivalent to {@link #copyTo(PermazenTransaction, 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 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(PermazenTransaction 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 will be migrated to match the source
* object if necessary (with resulting {@link OnSchemaChange @OnSchemaChange} notifications).
* If {@link CopyState#isSuppressNotifications()} returns false, {@link OnCreate @OnCreate}
* and {@link OnChange @OnChange} notifications will also be delivered.
*
*
* 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 schemas encountered, those schemas
* must be identical in both transactions.
*
*
* If {@code dest} is not a {@link DetachedPermazenTransaction} and any copied objects contain reference fields configured with
* {@link io.permazen.annotation.PermazenField#allowDeleted}{@code = false}, then any objects referenced by those fields must
* also be copied, or else must already exist in {@code dest}. 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 pobjs 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 UnknownTypeException if any source object's type does not exist in {@code dest}
* @throws 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(PermazenTransaction dest, CopyState copyState, Stream extends PermazenObject> pobjs) {
Preconditions.checkArgument(pobjs != null, "null pobjs");
this.copyIdStreamTo(dest, copyState, pobjs
.filter(Objects::nonNull)
.peek(PermazenTransaction::registerPermazenObject) // handle possible re-entrant object cache load
.map(PermazenObject::getObjId));
}
private void copyIdStreamTo(PermazenTransaction 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");
// Track deleted assignments while we copy
final ObjIdMap deletedAssignments = new ObjIdMap<>();
// Do the copy
ids.iterator().forEachRemaining(id -> this.copyTo(copyState, dest, id, deletedAssignments, true));
// If any deleted assignments remain, grab an arbitrary one and throw an exception
final Map.Entry entry = deletedAssignments.removeOne();
if (entry != null) {
final ObjId targetId = entry.getKey();
final DeletedAssignment deletedAssignment = entry.getValue();
final ObjId id = deletedAssignment.getId();
final ReferenceField field = deletedAssignment.getField();
throw new DeletedObjectException(targetId, String.format(
"illegal assignment of deleted object %s (%s) to %s in object %s (%s)",
targetId, this.tx.getTypeDescription(targetId), field, id, this.tx.getTypeDescription(id)));
}
}
void copyTo(CopyState copyState, PermazenTransaction dest, ObjId srcId,
ObjIdMap deletedAssignments, boolean required) {
// Already copied?
if (!copyState.markCopied(srcId))
return;
// Get source and destination PermazenClass
final PermazenClass> srcPClass = this.pdb.getPermazenClass(srcId);
final PermazenClass> dstPClass = dest.pdb.getPermazenClass(srcPClass.name);
// Optimization: see if we can disable listener notifications
boolean enableNotifications = !copyState.isSuppressNotifications();
if (enableNotifications && dest.isDetached() && dstPClass != null)
enableNotifications = !dstPClass.onCreateMethods.isEmpty() || !dstPClass.onChangeMethods.isEmpty();
// Copy object at the core API level
final ObjIdMap coreDeletedAssignments = new ObjIdMap<>();
try {
this.tx.copy(srcId, dest.tx, true, enableNotifications, coreDeletedAssignments, copyState.getObjectIdMap());
} catch (DeletedObjectException e) {
assert !this.exists(srcId);
if (required)
throw e;
return;
}
// Get destination ID
assert copyState.isCopied(srcId);
final ObjId dstId = copyState.getDestId(srcId);
assert dstId.equals(srcId) || copyState.getObjectIdMap().containsKey(srcId);
// Reset cached fields in the destination PermazenObject, if any
final PermazenObject dstObject = dest.pobjectCache.getIfExists(dstId);
if (dstObject != null)
dstObject.resetCachedFieldValues();
// Revalidate destination object if needed
if (dest.validationMode.equals(ValidationMode.AUTOMATIC) && dstPClass != null && dstPClass.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());
deletedAssignments.put(entry.getKey(), new DeletedAssignment(dstId, entry.getValue()));
}
// Remove the copied object from the deleted assignments set in our copy state.
// This fixes up "forward reference" deleted assignments.
deletedAssignments.remove(dstId);
}
/**
* Find all objects reachable through the specified reference cascades.
*
*
* This method finds all objects reachable from the given starting object through
* {@linkplain io.permazen.annotation.PermazenField#forwardCascades forward} and
* {@linkplain io.permazen.annotation.PermazenField#inverseCascades inverse} reference field cascades with the specified names.
* In other words, a reference field is traversed in the forward or inverse direction if any of the given {@code cascades}
* are found in the field's
* {@linkplain io.permazen.annotation.PermazenField#forwardCascades() @PermazenField.forwardCascades()} or
* {@linkplain io.permazen.annotation.PermazenField#inverseCascades() @PermazenField.inverseCascades()} annotation
* property, respectively.
*
*
* The {@code visited} set contains the ID's of objects already visited (or is empty if none); these objects will not
* be traversed. In particular, if {@code id} is in {@code visited}, then this method does nothing.
* Upon return, {@code visited} will have had all of the new objects found added.
*
*
* All new objects found will be {@linkplain #migrateSchema migrated} to the this transaction's schema if necessary.
*
*
* The {@code maxDistance} parameter can be used to limit the maximum distance of any reachable object, measured
* in the number of reference field hops from the starting object. If a value other than -1 is given, objects will
* be visited in breadth-first manner (i.e., ordered by distance) and the search is truncated at {@code maxDistance}
* hops. If -1 is given, there is no limit and also no implied ordering of the objects in the iteration.
*
* @param id starting object ID
* @param maxDistance the maximum number of reference fields to hop through, or -1 for no limit
* @param visited on entry objects already visited, on return all objects reached
* @param cascades zero or more reference cascade names
* @return iteration of reachable objects
* @throws DeletedObjectException if any object containing a traversed reference field does not actually exist
* @throws IllegalArgumentException if {@code maxDistance} is less than -1
* @throws IllegalArgumentException if any parameter is null
* @see PermazenObject#cascade PermazenObject.cascade()
* @see io.permazen.annotation.PermazenField#forwardCascades @PermazenField.forwardCascades()
* @see io.permazen.annotation.PermazenField#inverseCascades @PermazenField.inverseCascades()
*/
public Iterator cascade(ObjId id, int maxDistance, ObjIdSet visited, String... cascades) {
// Sanity check
Preconditions.checkArgument(id != null, "null id");
Preconditions.checkArgument(cascades != null, "null cascades");
Preconditions.checkArgument(maxDistance >= -1, "maxDistance < -1");
Preconditions.checkArgument(visited != null, "null visited");
for (String cascade : cascades)
Preconditions.checkArgument(cascade != null, "null cascade");
// Build initial set
final ObjIdSet initial = new ObjIdSet();
if (visited.add(id))
initial.add(id);
// Handle breadth-first vs. unordered
if (maxDistance >= 0) {
return new AbstractIterator() {
private ObjIdSet currLevel = initial;
private ObjIdSet nextLevel = new ObjIdSet();
private int remainingHops = maxDistance;
@Override
protected ObjId computeNext() {
// Any remaining at the current level?
if (!this.currLevel.isEmpty()) {
final ObjId next = this.currLevel.removeOne();
if (this.remainingHops > 0) // don't expand past the distance limit
PermazenTransaction.this.gatherCascadeRefs(next, cascades, visited, this.nextLevel);
return next;
}
// Take it to the next level
final ObjIdSet empty = this.currLevel;
this.currLevel = this.nextLevel;
this.nextLevel = empty;
this.remainingHops--;
// Anything there?
if (this.currLevel.isEmpty())
return this.endOfData();
// Continue
return this.computeNext();
}
};
} else {
return new AbstractIterator() {
@Override
protected ObjId computeNext() {
if (!initial.isEmpty()) {
final ObjId next = initial.removeOne();
PermazenTransaction.this.gatherCascadeRefs(next, cascades, visited, initial);
return next;
}
return this.endOfData();
}
};
}
}
// Cascade from object
private void gatherCascadeRefs(ObjId id, String[] cascades, ObjIdSet visited, ObjIdSet dest) {
// Migrate schema if needed
this.tx.migrateSchema(id);
// Get object type
final PermazenClass> pclass = this.pdb.getPermazenClass(id.getStorageId());
// Gather references
this.gatherForwardCascadeRefs(pclass, id, cascades, visited, dest);
this.gatherInverseCascadeRefs(pclass, id, cascades, visited, dest);
}
// Cascade forward from object
private void gatherForwardCascadeRefs(PermazenClass> pclass, ObjId id, String[] cascades, ObjIdSet visited, ObjIdSet dest) {
for (String cascade : cascades) {
final List fields = pclass.forwardCascadeMap.get(cascade);
if (fields == null)
continue;
fields.forEach(field -> this.addRefs(field.iterateReferences(this.tx, id), visited, dest));
}
}
// Cascade inversely from object
@SuppressWarnings("unchecked")
private void gatherInverseCascadeRefs(PermazenClass> pclass, ObjId id, String[] cascades, ObjIdSet visited, ObjIdSet dest) {
for (String cascade : cascades) {
final Map refMap = pclass.inverseCascadeMap.get(cascade);
if (refMap == null)
continue;
refMap.forEach((storageId, refTypeRanges) -> {
// Access the index associated with the reference field
CoreIndex1 index = (CoreIndex1)this.tx.querySimpleIndex(storageId);
// Restrict references from objects containing the field with the cascade (precomputed via "refTypeRanges")
index = index.filter(1, refTypeRanges);
// Find objects referring to "id" through the field
final NavigableSet refs = index.asMap().get(id);
if (refs != null)
this.addRefs(refs, visited, dest);
});
}
}
private void addRefs(Iterable refs, ObjIdSet visited, ObjIdSet dest) {
try (CloseableIterator i = CloseableIterator.wrap(refs.iterator())) {
while (i.hasNext()) {
final ObjId ref = i.next();
if (ref != null && visited.add(ref))
dest.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
* by this transaction.
*
*
* A non-null object is always returned, but the corresponding object may not actually exist in this transaction.
* If not, attempts to access its fields will throw {@link DeletedObjectException}.
* Use {@link PermazenObject#exists PermazenObject.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 UntypedPermazenObject} is returned.
*
* @param id object ID
* @return Java model object
* @throws IllegalArgumentException if {@code id} is null
* @see #get(ObjId, Class)
* @see #get(PermazenObject)
*/
public PermazenObject get(ObjId id) {
return this.pobjectCache.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
* by this transaction.
*
*
* A non-null object is always returned, but the corresponding object may not actually exist in this transaction.
* If not, attempts to access its fields will throw {@link DeletedObjectException}.
* Use {@link PermazenObject#exists PermazenObject.exists()} to check.
*
*
* This method just invokes {@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 either parameter is null
* @see #get(ObjId)
* @see #get(PermazenObject)
*/
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 PermazenObject} 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.
* If not, attempts to access its fields will throw {@link DeletedObjectException}.
* Use {@link PermazenObject#exists PermazenObject.exists()} to check.
*
*
* This method is equivalent to {@code get(pobj.getObjId())} followed by an appropriate cast to type {@code T}.
*
* @param pobj Java model object
* @param expected Java type
* @return Java model object in this transaction with the same object ID (possibly {@code pobj} itself)
* @throws IllegalArgumentException if {@code pobj} 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 pobj}
* @see #get(ObjId)
* @see #get(ObjId, Class)
*/
@SuppressWarnings("unchecked")
public T get(T pobj) {
return (T)pobj.getModelClass().cast(this.get(pobj.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.pdb.getPermazenClass(type));
}
/**
* Create a new instance of the given type in this transaction.
*
* @param pclass object type
* @param Java model type
* @return newly created instance
* @throws IllegalArgumentException if {@code pclass} is null or not valid for this instance
* @throws StaleTransactionException if this transaction is no longer usable
*/
public T create(PermazenClass pclass) {
Preconditions.checkArgument(pclass != null, "null pclass");
final ObjId id = this.tx.create(pclass.name);
return pclass.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 PermazenObject#delete} would be used instead.
*
* @param pobj 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 DeleteAction#EXCEPTION}
* @throws StaleTransactionException if this transaction is no longer usable
* @throws IllegalArgumentException if {@code pobj} is null
*/
public boolean delete(PermazenObject pobj) {
// Sanity check
Preconditions.checkArgument(pobj != null, "null pobj");
// Handle possible re-entrant object cache load
PermazenTransaction.registerPermazenObject(pobj);
// Delete object
final ObjId id = pobj.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)
pobj.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 PermazenObject#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 IllegalArgumentException 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 PermazenObject#recreate} would be used instead.
*
* @param pobj 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 IllegalArgumentException if {@code pobj} is null
*/
public boolean recreate(PermazenObject pobj) {
PermazenTransaction.registerPermazenObject(pobj); // handle possible re-entrant object cache load
return this.tx.create(pobj.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 PermazenObject#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 jakarta.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.isOpen())
throw new StaleTransactionException(this.tx);
this.validationQueue.clear();
}
private synchronized void revalidate(Collection extends ObjId> ids, Class>... groups) {
// Sanity checks
if (!this.tx.isOpen())
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()]));
}
}
/**
* Update the schema of the specified object, if necessary, so that it matches
* the schema associated with this instance's {@link Permazen}.
*
*
* If a schema change occurs, {@link OnSchemaChange @OnSchemaChange} methods will be invoked
* prior to this method returning.
*
*
* This method is typically only used by generated classes; normally, {@link PermazenObject#migrateSchema}
* would be used instead.
*
* @param pobj object to update
* @return true if the object's schema was migrated, false if it was already updated
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if {@code pobj} does not exist in this transaction
* @throws TypeNotInSchemaException if the current schema does not contain the object's type
* @throws IllegalArgumentException if {@code pobj} is null
*/
public boolean migrateSchema(PermazenObject pobj) {
PermazenTransaction.registerPermazenObject(pobj); // handle possible re-entrant object cache load
return this.tx.migrateSchema(pobj.getObjId());
}
/**
* Ensure the given {@link PermazenObject} 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 PermazenObject} is fully constructed and associated with its {@link PermazenTransaction}.
*
* @param pobj object to register
* @throws IllegalArgumentException if {@code pobj} is null
*/
public static void registerPermazenObject(PermazenObject pobj) {
pobj.getPermazenTransaction().pobjectCache.register(pobj);
}
/**
* Read a simple field.
*
*
* This returns the value returned by {@link Transaction#readSimpleField Transaction.readSimpleField()}
* with {@link ObjId}s converted into {@link PermazenObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenField @PermazenField} getter override methods
* and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param fieldName the name of the {@link PermazenSimpleField}
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @return the 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 PermazenSimpleField} corresponding to {@code fieldName} in {@code id} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but the object has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code id} or {@code fieldName} is null
*/
public Object readSimpleField(ObjId id, String fieldName, boolean migrateSchema) {
return this.convert(
this.pdb.getField(id, fieldName, PermazenSimpleField.class).getConverter(this),
this.tx.readSimpleField(id, fieldName, migrateSchema));
}
/**
* Write a simple field.
*
*
* This writes the value via {@link Transaction#writeSimpleField Transaction.writeSimpleField()}
* after converting {@link PermazenObject}s into {@link ObjId}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenField @PermazenField} setter override methods
* and not normally invoked directly by user code.
*
* @param pobj object containing the field
* @param fieldName the name of the {@link PermazenSimpleField}
* @param value new value for the field
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @throws StaleTransactionException if this transaction is no longer usable
* @throws DeletedObjectException if {@code pobj} does not exist in this transaction
* @throws UnknownFieldException if no {@link PermazenSimpleField} corresponding to {@code fieldName} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but {@code pobj} has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code value} is not an appropriate value for the field
* @throws IllegalArgumentException if {@code pobj} or {@code fieldName} is null
*/
public void writeSimpleField(PermazenObject pobj, String fieldName, Object value, boolean migrateSchema) {
PermazenTransaction.registerPermazenObject(pobj); // handle possible re-entrant object cache load
final ObjId id = pobj.getObjId();
final Converter, ?> converter = this.pdb.getField(id, fieldName, PermazenSimpleField.class).getConverter(this);
if (converter != null)
value = this.convert(converter.reverse(), value);
this.tx.writeSimpleField(id, fieldName, value, migrateSchema);
}
/**
* Read a counter field.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenField @PermazenField} getter override methods
* and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param fieldName the name of the {@link PermazenCounterField}
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @return the 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 PermazenCounterField} corresponding to {@code fieldName} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but the object has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code id} or {@code fieldName} is null
*/
public Counter readCounterField(ObjId id, String fieldName, boolean migrateSchema) {
this.pdb.getField(id, fieldName, PermazenCounterField.class); // validate encoding
if (migrateSchema)
this.tx.migrateSchema(id);
return new Counter(this.tx, id, fieldName, migrateSchema);
}
/**
* Read a set field.
*
*
* This returns the set returned by {@link Transaction#readSetField Transaction.readSetField()} with
* {@link ObjId}s converted into {@link PermazenObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenSetField @PermazenSetField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param fieldName the name of the {@link PermazenSetField}
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @return the 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 PermazenSetField} corresponding to {@code fieldName} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but the object has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code id} or {@code fieldName} is null
*/
public NavigableSet> readSetField(ObjId id, String fieldName, boolean migrateSchema) {
return this.convert(
this.pdb.getField(id, fieldName, PermazenSetField.class).getConverter(this),
this.tx.readSetField(id, fieldName, migrateSchema));
}
/**
* Read a list field.
*
*
* This returns the list returned by {@link Transaction#readListField Transaction.readListField()} with
* {@link ObjId}s converted into {@link PermazenObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenListField @PermazenListField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param fieldName the name of the {@link PermazenListField}
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @return the 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 PermazenListField} corresponding to {@code fieldName} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but the object has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code id} or {@code fieldName} is null
*/
public List> readListField(ObjId id, String fieldName, boolean migrateSchema) {
return this.convert(
this.pdb.getField(id, fieldName, PermazenListField.class).getConverter(this),
this.tx.readListField(id, fieldName, migrateSchema));
}
/**
* Read a map field.
*
*
* This returns the map returned by {@link Transaction#readMapField Transaction.readMapField()} with
* {@link ObjId}s converted into {@link PermazenObject}s, etc.
*
*
* This method is used by generated {@link io.permazen.annotation.PermazenMapField @PermazenMapField}
* getter override methods and not normally invoked directly by user code.
*
* @param id ID of the object containing the field
* @param fieldName the name of the {@link PermazenMapField}
* @param migrateSchema true to automatically migrate the object's schema, false to not change it
* @return the 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 PermazenMapField} corresponding to {@code fieldName} exists
* @throws TypeNotInSchemaException if {@code migrateSchema} is true but the object has a type
* that does not exist in this instance's schema
* @throws IllegalArgumentException if {@code id} or {@code fieldName} is null
*/
public NavigableMap, ?> readMapField(ObjId id, String fieldName, boolean migrateSchema) {
return this.convert(
this.pdb.getField(id, fieldName, PermazenMapField.class).getConverter(this),
this.tx.readMapField(id, fieldName, migrateSchema));
}
// Reference Path Access
/**
* Find all target 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, Stream extends PermazenObject> startObjects) {
Preconditions.checkArgument(path != null, "null path");
Preconditions.checkArgument(startObjects != null, "null startObjects");
final NavigableSet ids = this.tx.followReferencePath(startObjects.map(this.referenceConverter),
path.getReferenceFields(), path.getPathKeyRanges());
return new ConvertedNavigableSet(ids, this.referenceConverter);
}
/**
* Find all starting 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, Stream extends PermazenObject> targetObjects) {
Preconditions.checkArgument(path != null, "null path");
Preconditions.checkArgument(targetObjects != null, "null targetObjects");
final NavigableSet ids = this.tx.invertReferencePath(path.getReferenceFields(), path.getPathKeyRanges(),
targetObjects.map(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"})
* unless there is only one sub-field (i.e., sets and lists but not maps)
* @param valueType the Java type corresponding to the field value
* @param Java type containing the field
* @param Java type corresponding to the indexed 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 Index1 querySimpleIndex(Class targetType, String fieldName, Class valueType) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(new IndexQuery.Key(fieldName, false, targetType, valueType));
final PermazenSimpleField pfield = (PermazenSimpleField)info.schemaItem;
final CoreIndex1, ObjId> index = info.applyFilters(this.tx.querySimpleIndex(pfield.storageId));
final Converter, ?> valueConverter = Util.reverse(pfield.getConverter(this));
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex1(index, valueConverter, targetConverter);
}
/**
* Get the composite index on a list field that includes list indices.
*
* @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 containing the field
* @param Java type corresponding to the indexed list's element field
* @return read-only, real-time view of field values, objects having that value in the field, and corresponding list indices
* @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) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(new IndexQuery.Key(fieldName, false, targetType, valueType));
final PermazenSimpleField pfield = (PermazenSimpleField)info.schemaItem;
if (!(pfield.getParentField() instanceof PermazenListField))
throw new IllegalArgumentException(String.format("field \"%s\" is not a list element sub-field", fieldName));
final CoreIndex2, ObjId, Integer> index = info.applyFilters(this.tx.queryListElementIndex(pfield.storageId));
final Converter, ?> valueConverter = Util.reverse(pfield.getConverter(this));
final Converter indexConverter = Converter.identity();
final Converter targetConverter = new ReferenceConverter(this, targetType);
return new ConvertedIndex2(index, valueConverter, targetConverter, indexConverter);
}
/**
* 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 containing the field
* @param Java type corresponding to the indexed map's value 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) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(
new IndexQuery.Key(fieldName, false, targetType, valueType, keyType));
final PermazenSimpleField pfield = (PermazenSimpleField)info.schemaItem;
final PermazenMapField parentField;
if (!(pfield.getParentField() instanceof PermazenMapField)
|| pfield != (parentField = (PermazenMapField)pfield.getParentField()).valueField)
throw new IllegalArgumentException(String.format("field \"%s\" is not a map value sub-field", fieldName));
final CoreIndex2, ObjId, ?> index = info.applyFilters(this.tx.queryMapValueIndex(pfield.storageId));
final Converter, ?> valueConverter = Util.reverse(pfield.getConverter(this));
final Converter, ?> keyConverter = Util.reverse(parentField.keyField.getConverter(this));
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 containing the field
* @param Java type corresponding to the first indexed field
* @param Java type corresponding to the second indexed 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) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(
new IndexQuery.Key(indexName, true, targetType, value1Type, value2Type));
final PermazenCompositeIndex pindex = (PermazenCompositeIndex)info.schemaItem;
final CoreIndex2, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex2(pindex.storageId));
final Converter, ?> value1Converter = Util.reverse(pindex.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(pindex.pfields.get(1).getConverter(this));
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 containing the field
* @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
* @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) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(
new IndexQuery.Key(indexName, true, targetType, value1Type, value2Type, value3Type));
final PermazenCompositeIndex pindex = (PermazenCompositeIndex)info.schemaItem;
final CoreIndex3, ?, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex3(pindex.storageId));
final Converter, ?> value1Converter = Util.reverse(pindex.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(pindex.pfields.get(1).getConverter(this));
final Converter, ?> value3Converter = Util.reverse(pindex.pfields.get(2).getConverter(this));
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 containing the field
* @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
* @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) {
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
final IndexQuery info = this.pdb.getIndexQuery(
new IndexQuery.Key(indexName, true, targetType, value1Type, value2Type, value3Type, value4Type));
final PermazenCompositeIndex pindex = (PermazenCompositeIndex)info.schemaItem;
final CoreIndex4, ?, ?, ?, ObjId> index = info.applyFilters(this.tx.queryCompositeIndex4(pindex.storageId));
final Converter, ?> value1Converter = Util.reverse(pindex.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(pindex.pfields.get(1).getConverter(this));
final Converter, ?> value3Converter = Util.reverse(pindex.pfields.get(2).getConverter(this));
final Converter, ?> value4Converter = Util.reverse(pindex.pfields.get(3).getConverter(this));
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 PermazenSimpleField}'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 Index> queryIndex(int storageId) {
// Check transcaction
if (!this.tx.isOpen())
throw new StaleTransactionException(this.tx);
// Find index
final PermazenSchemaItem schemaItem = this.pdb.indexesByStorageId.get(storageId);
if (schemaItem == null) {
throw new IllegalArgumentException(String.format(
"no composite index or simple indexed field exists with storage ID %d", storageId));
}
// Handle a composite index
if (schemaItem instanceof PermazenCompositeIndex) {
final PermazenCompositeIndex index = (PermazenCompositeIndex)schemaItem;
switch (index.pfields.size()) {
case 2:
{
final Converter, ?> value1Converter = Util.reverse(index.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(index.pfields.get(1).getConverter(this));
return new ConvertedIndex2(this.tx.queryCompositeIndex2(index.storageId),
value1Converter, value2Converter, this.referenceConverter);
}
case 3:
{
final Converter, ?> value1Converter = Util.reverse(index.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(index.pfields.get(1).getConverter(this));
final Converter, ?> value3Converter = Util.reverse(index.pfields.get(2).getConverter(this));
return new ConvertedIndex3(this.tx.queryCompositeIndex3(index.storageId),
value1Converter, value2Converter, value3Converter, this.referenceConverter);
}
case 4:
{
final Converter, ?> value1Converter = Util.reverse(index.pfields.get(0).getConverter(this));
final Converter, ?> value2Converter = Util.reverse(index.pfields.get(1).getConverter(this));
final Converter, ?> value3Converter = Util.reverse(index.pfields.get(2).getConverter(this));
final Converter, ?> value4Converter = Util.reverse(index.pfields.get(3).getConverter(this));
return new ConvertedIndex4(this.tx.queryCompositeIndex4(index.storageId),
value1Converter, value2Converter, value3Converter, value4Converter, this.referenceConverter);
}
// COMPOSITE-INDEX
default:
throw new RuntimeException("internal error");
}
}
// Must be a simple field index
return ((PermazenSimpleField)schemaItem).getIndex(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.RetryKVTransactionException from {@link 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.isOpen())
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#isOpen
*/
public boolean isOpen() {
return this.tx.isOpen();
}
/**
* 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 this transaction was created with {@link ValidationMode#DISABLED}, then this method does nothing.
*
* @throws io.permazen.kv.RetryKVTransactionException from {@link 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 PermazenObject#revalidate
*/
public void validate() {
// Sanity check
if (!this.tx.isOpen())
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 PermazenTransaction previous = CURRENT.get();
CURRENT.set(this);
try {
action.run();
} finally {
CURRENT.set(previous);
}
}
/**
* Invoke the given {@link Supplier} 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
* @return result from action
* @throws IllegalArgumentException if {@code action} is null
*/
public T performAction(Supplier action) {
Preconditions.checkArgument(action != null, "null action");
final PermazenTransaction previous = CURRENT.get();
CURRENT.set(this);
try {
return action.get();
} finally {
CURRENT.set(previous);
}
}
/**
* Apply weaker transaction consistency while performing the given action, if supported.
*
*
* Some key/value implementations support reads with weaker consistency guarantees. These reads generate fewer
* transaction conflicts but return possibly out-of-date information. Depending on the implementation, when operating
* in this mode writes may not be supported and may generate a {@link IllegalStateException} or just be ignored.
*
*
* The weaker consistency is only applied for the current thread, and it ends when this method returns.
*
*
* This method is for experts only; inappropriate use can result in a corrupted database.
* You should not make any changes to the database after this method returns based on any information
* read by the {@code action}.
*
* @param action the action to perform
* @throws IllegalArgumentException if {@code action} is null
*/
public void withWeakConsistency(Runnable action) {
this.tx.withWeakConsistency(action);
}
// Internal methods
@SuppressWarnings("unchecked")
private void doValidate() {
final ValidatorFactory validatorFactory = this.pdb.validatorFactory;
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 the object 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 PermazenObject pobj = this.get(id);
final PermazenClass> pclass = this.pdb.pclassesByStorageId.get(id.getStorageId());
if (pclass == null)
return;
// Do early @OnValidate method validation, bailing out if an @OnValidate method deletes the object
if (!this.doOnValidate(pclass.earlyOnValidateMethods, pobj, validationGroups))
continue;
// Do singleton validation
if (pclass.singleton) {
final AbstractKVNavigableSet ids = (AbstractKVNavigableSet)this.tx.getAll(pclass.name);
try (CloseableIterator i = ids.iterator()) {
while (i.hasNext()) {
final ObjId id2 = i.next();
if (id2.equals(id))
continue;
throw new ValidationException(pobj, String.format(
"singleton constraint on type \"%s\" failed for object %s: object %s also exists",
pclass.name, id, id2));
}
}
}
// Do JSR 303 validation (if any)
if (validator != null && pclass.elementRequiringJSR303Validation != null) {
// Run validator
final Set> violations;
try {
violations = new ValidationContext(pobj, 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(pobj, violations, String.format(
"validation error for object %s of type \"%s\":%n%s",
id, pclass.name, ValidationUtil.describe(violations)));
}
// It's posible (though unlikely) that a JSR 303 validation could have deleted the object, so check for that
if (!this.tx.exists(id))
continue;
}
// Do simple and composite field uniqueness validation
if ((!pclass.uniqueConstraintFields.isEmpty() || !pclass.uniqueConstraintCompositeIndexes.isEmpty())
&& Util.isAnyGroupBeingValidated(DEFAULT_AND_UNIQUENESS_CLASS_ARRAY, validationGroups)) {
// Check simple index uniqueness constraints
for (PermazenSimpleField pfield : pclass.uniqueConstraintFields) {
assert pfield.indexed;
assert pfield.unique;
// Get field's (core API) value
final Object value = this.tx.readSimpleField(id, pfield.name, false);
// Is this value excluded from the uniqueness constraint?
if (pfield.uniqueExcludes != null && pfield.uniqueExcludes.matches(value))
continue;
// Query core API index to find other objects with the same value in the field, but restrict the search to
// only include those types having the annotated method, not some other method with the same name/storage ID.
final IndexQuery info = this.pdb.getIndexQuery(new IndexQuery.Key(pfield));
final CoreIndex1, ObjId> index = info.applyFilters(this.tx.querySimpleIndex(pfield.storageId));
// Seach for other objects with the same value in the field and report violation if any are found
final List conflictors = this.findUniqueConflictors(id, index.asMap().get(value));
if (!conflictors.isEmpty()) {
throw new ValidationException(pobj, String.format(
"uniqueness constraint on %s failed for object %s: field value %s is also shared by object(s) %s",
pfield, id, value, conflictors));
}
}
// Check composite index uniqueness constraints
compositeIndexUniqueLoop:
for (PermazenCompositeIndex pindex : pclass.uniqueConstraintCompositeIndexes) {
assert pindex.unique;
// Get field (core API) values
final int numFields = pindex.pfields.size();
final List