All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.permazen.Permazen Maven / Gradle / Ivy

The newest version!

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package io.permazen;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;

import io.permazen.annotation.PermazenType;
import io.permazen.core.Database;
import io.permazen.core.DetachedTransaction;
import io.permazen.core.InvalidSchemaException;
import io.permazen.core.ObjId;
import io.permazen.core.Schema;
import io.permazen.core.SchemaBundle;
import io.permazen.core.Transaction;
import io.permazen.core.TransactionConfig;
import io.permazen.core.UnknownFieldException;
import io.permazen.core.UnknownTypeException;
import io.permazen.kv.CloseableKVStore;
import io.permazen.kv.KVDatabase;
import io.permazen.kv.KVStore;
import io.permazen.kv.KVTransaction;
import io.permazen.kv.KVTransactionException;
import io.permazen.kv.KeyRange;
import io.permazen.kv.KeyRanges;
import io.permazen.kv.mvcc.BranchedKVTransaction;
import io.permazen.kv.mvcc.TransactionConflictException;
import io.permazen.kv.util.MemoryKVStore;
import io.permazen.schema.SchemaId;
import io.permazen.schema.SchemaItem;
import io.permazen.schema.SchemaModel;
import io.permazen.tuple.Tuple2;
import io.permazen.util.ApplicationClassLoader;

import jakarta.validation.MessageInterpolator;
import jakarta.validation.Validation;
import jakarta.validation.ValidatorFactory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Permazen Java persistence layer.
 *
 * 

* Permazen is a Java persistence solution built on three layers of abstraction: *

    *
  • At the bottom is a simple Key/Value Layer represented by {@link KVDatabase}. * Transactions are supported at this layer and are accessed via {@link KVTransaction}. * There are several available {@link KVDatabase} implementations, including wrappers * for many third party key/value stores.
  • *
  • On top of that sits the Core API Layer, which provides a rigorous "object database" abstraction on top of * a {@link KVDatabase}. It supports a flat hierarchy of "object" types, where an object consists of fields that are * either simple (i.e., any Java type that can be (de)serialized to/from a {@code byte[]} array), list, set, or map. * It also includes tightly controlled schema tracking, simple and composite indexes, and lifecycle and change notifications. * It is not Java-specific or explicitly object-oriented: an "object" at this layer is just a structure with defined fields. * The core API layer may be accessed through the {@link Database} and {@link Transaction} classes.
  • *
  • The Java Layer is a Java-centric, object-oriented persistence layer for Java applications. * It sits on top of the core API layer and provides a fully "Java" view of the underlying data where all data access * is through user-supplied Java model classes. All schema definition and listener registrations are inferred from * {@linkplain io.permazen.annotation Java annotations}. Incremental JSR 303 validation is supported. * The {@link Permazen} class represents an instance of this top layer database, and {@link PermazenTransaction} * represents the corresonding transactions.
  • *
* *

* User-provided Java model classes define database fields by declaring abstract Java bean methods; * {@link Permazen} generates concrete subclasses of the user-provided abstract model classes at runtime. * These runtime classes will implement the Java bean methods as well as the {@link PermazenObject} interface. * Instances of Java model classes are always associated with a specific {@link PermazenTransaction}, and all of * their state derives from the underlying key/value {@link KVTransaction}. * *

* Java model class instances have a unique {@link ObjId} which represents database identity. {@link Permazen} guarantees that * at most one Java instance will exist for any given {@link PermazenTransaction} and {@link ObjId}. * *

Transactions * *

* New instance creation, index queries, and certain other database-related tasks are initiated through a * {@link PermazenTransaction}. Normal transactions are created via {@link #createTransaction createTransaction()}. * *

Detached Transactions * *

* Detached transactions are in-memory transactions that are completely detached from the database. They are never * committed and may persist indefinitely. Their purpose is to hold a database objects in memory where they can be manipulated * like normal Java objects, but with the usual functionality available to open transactions added, such as index queries, * schema tracking, change notifications, reference cascades, etc. Detached transactions are initially empty, and are often * used to hold a copy of some small portion of the database. Because their state is encoded entirely by in-memory key/value * pairs, they are easily serialized/deserialized. They are also useful in non-database applications where a Java data * structure in which all reference fields are invertable is needed. * *

* See {@link PermazenTransaction#createDetachedTransaction PermazenTransaction.createDetachedTransaction()}, * {@link PermazenTransaction#getDetachedTransaction}, {@link PermazenObject#copyOut PermazenObject.copyOut()}, and * {@link PermazenObject#copyIn PermazenObject.copyIn()}. * *

Branched Transactions * *

* Branched transactions behave like normal transactions, but internally they are constructed from two separate * key/value transactions, one that occurs when the branched transaction is opened and one that occurs at commit time. * When a branched transaction is opened, the first key/value transaction is opened just long enoughh to create a read-only * {@linkplain KVTransaction#readOnlySnapshot snapshot} of the database and then closed. The branched transaction then * operates entirely in memory, based on the snapshot, and it keeps track of all keys read and written. At commit time, * a new key/value transaction is opened and a conflict check is performed. If successful, the accumulated writes are * flushed out and the transaction completes, otherwise a {@link TransactionConflictException} is thrown. * *

* Because they consume no database resources while open, branched transactions can stay open for an arbitrarily long * time, yet they still provide the same consistency guarantees as normal transactions. Of course this comes at the cost of * having to track reads and writes in memory and performing conflict checks all at once on commit. Howerver they can be useful * in certain scenarios. For example, imagine a user is editing some database object in a GUI application, and the user could * take several minutes to complete a form. Because only a single object is being edited, the operation is guaranteed to * only access a small amount of data. Using a branched transaction means no database resources are held open while the * user decides. * *

* In addition, in many applications it's important to detect conflicts, for example, when two users edit the same object * where the second user's changes would otherwise overwite the first user's changes. Traditionally this kind of conflict check * is done at the application layer, either by using two separate transactions and applying application-level conflict * detection (which is tedious and error-prone), or by relying on database object versioning to detect unexpected changes, * which is an incomplete solution because it fails to detect changes in other objects that might have been viewed by * the user when deciding what to change (e.g., some other object that the edited object refers to). In other words, traditional * database object versioning detects write/write conflicts but not read/write conflicts, and only for the object being * written back. Branched transactions provide full consistency in a completely automated way by guaranteeing that all * information the user sees while editing a form is still valid when the form's changes are committed. * *

* Branched transactions require that the underlying key/value database support {@link KVTransaction#readOnlySnapshot} * and require keeping track of every database read and write during the transaction; see {@link BranchedKVTransaction} * for details and other caveats. They are created via {@link #createBranchedTransaction() createBranchedTransaction()}. * *

Initialization * *

* Instances of this class must be {@linkplain #initialize initialized} before use. This involves accessing the underlying * database and registering the Java data model's schema. This will happen automatically (if needed) when any method that * requires the registered schema is invoked (including creating a transaction), or by explicit invocation of {@link #initialize}. * See also {@link PermazenConfig.Builder#initializeOnCreation(boolean) PermazenConfig.Builder.initializeOnCreation()}. * * @see PermazenObject * @see PermazenTransaction * @see PermazenConfig * @see io.permazen.annotation * @see Permazen GitHub Page */ @ThreadSafe public class Permazen { /** * The suffix that is appended to Java model class names to get the corresponding Permazen generated class name. */ public static final String GENERATED_CLASS_NAME_SUFFIX = "$$Permazen"; /** * The version of this library. */ public static final String VERSION; private static final String PROPERTIES_RESOURCE = "/META-INF/permazen/permazen.properties"; private static final String VERSION_PROPERTY_NAME = "permazen.version"; static { final Properties properties = new Properties(); try (InputStream input = Permazen.class.getResourceAsStream(PROPERTIES_RESOURCE)) { if (input == null) throw new RuntimeException(String.format("can't find resource %s", PROPERTIES_RESOURCE)); properties.load(input); } catch (IOException e) { throw new RuntimeException("unexpected exception", e); } VERSION = properties.getProperty(VERSION_PROPERTY_NAME, "?"); } private static final int MAX_INDEX_QUERY_INFO_CACHE_SIZE = 1000; private static final String HIBERNATE_PARAMETER_MESSAGE_INTERPOLATOR_CLASS_NAME = "org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator"; final Logger log = LoggerFactory.getLogger(this.getClass()); final ArrayList> pclasses = new ArrayList<>(); final TreeMap> pclassesByName = new TreeMap<>(); final HashMap, PermazenClass> pclassesByType = new HashMap<>(); final TreeMap> pclassesByStorageId = new TreeMap<>(); final HashMap schemaItemsBySchemaId = new HashMap<>(); final HashSet fieldsRequiringDefaultValidation = new HashSet<>(); final HashMap indexesByStorageId = new HashMap<>(); // contains REPRESENTATIVE schema items final HashMap, PermazenField> typeFieldMap = new HashMap<>(); @SuppressWarnings("this-escape") final ReferencePathCache referencePathCache = new ReferencePathCache(this); final ClassGenerator untypedClassGenerator; final ArrayList> classGenerators; final ClassLoader loader = new Loader(); final ValidatorFactory validatorFactory; final SchemaModel origSchemaModel; // does not include storage ID assignments final SchemaModel schemaModel; // includes storage ID assignments final Database db; // Cached listener sets used by PermazenTransaction.() final Transaction.ListenerSet[] listenerSets = new Transaction.ListenerSet[4]; @GuardedBy("this") boolean initializing; @GuardedBy("this") boolean initialized; @SuppressWarnings("this-escape") private final LoadingCache indexQueryCache = CacheBuilder.newBuilder() .maximumSize(MAX_INDEX_QUERY_INFO_CACHE_SIZE) .build(CacheLoader.from(key -> key.getIndexQuery(this))); // Constructor /** * Create a new instance using the given configuration. * * @param config configuration to use * @throws IllegalArgumentException if {@code config} contains a null class or a class with invalid annotation(s) * @throws IllegalArgumentException if {@code config} is null * @throws InvalidSchemaException if the schema implied by {@code classes} is invalid */ @SuppressWarnings("this-escape") public Permazen(PermazenConfig config) { final ClassLoader prevLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(ApplicationClassLoader.getInstance()); try { // Initialize Preconditions.checkArgument(config != null, "null config"); this.db = config.getDatabase(); // Inventory classes; automatically add all @PermazenType-annotated superclasses of @PermazenType-annotated classes final HashMap, PermazenType> permazenTypes = new HashMap<>(); for (Class type : config.getModelClasses()) { do { // Find @PermazenType annotation final PermazenType annotation = Util.getAnnotation(type, PermazenType.class); if (annotation == null) continue; // Sanity check type if (type.isPrimitive() || type.isArray()) { throw new IllegalArgumentException(String.format( "illegal type %s for @%s annotation: not a normal class or interface", type, PermazenType.class.getSimpleName())); } // Add class permazenTypes.put(type, annotation); } while ((type = type.getSuperclass()) != null); } // Create PermazenClass objects permazenTypes.forEach((type, annotation) -> { // Get object type name final String typeName = !annotation.name().isEmpty() ? annotation.name() : type.getSimpleName(); if (this.log.isTraceEnabled()) { this.log.trace("found @{} annotation on {} defining object type \"{}\"", PermazenType.class.getSimpleName(), type, typeName); } // Check for name conflict final PermazenClass other = this.pclassesByName.get(typeName); if (other != null) { throw new IllegalArgumentException(String.format( "illegal duplicate use of object type name \"%s\" for both %s and %s", typeName, other.type.getName(), type.getName())); } // Create PermazenClass final PermazenClass pclass; try { pclass = new PermazenClass<>(this, typeName, annotation.storageId(), type); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(String.format( "invalid @%s annotation on %s: %s", PermazenType.class.getSimpleName(), type, e), e); } // Add PermazenClass this.pclassesByName.put(pclass.name, pclass); this.pclassesByType.put(pclass.type, pclass); // this should never conflict, no need to check if (typeName.equals(type.getSimpleName())) this.log.debug("added Java model class for object type \"{}\"", typeName); else this.log.debug("added Java model class {} for object type \"{}\"", type, typeName); }); this.pclassesByName.values().forEach(this.pclasses::add); // note: this.pclasses will be sorted by name // Inventory class generators this.classGenerators = this.pclasses.stream() .map(pclass -> pclass.classGenerator) .collect(Collectors.toCollection(ArrayList::new)); this.untypedClassGenerator = new ClassGenerator<>(this, UntypedPermazenObject.class); this.classGenerators.add(this.untypedClassGenerator); // Create fields this.pclasses.forEach(pclass -> pclass.createFields(this.db.getEncodingRegistry(), this.pclasses)); // Create composite indexes this.pclasses.forEach(PermazenClass::createCompositeIndexes); // Build and validate initial schema model this.schemaModel = new SchemaModel(); this.pclassesByName.forEach((name, pclass) -> this.schemaModel.getSchemaObjectTypes().put(name, pclass.toSchemaItem())); this.schemaModel.lockDown(false); this.schemaModel.validate(); if (!this.schemaModel.isEmpty()) this.log.debug("Permazen schema generated from annotated classes:\n{}", this.schemaModel); // Copy it now so we have a pre-storage ID assignment version this.origSchemaModel = this.schemaModel.clone(); this.origSchemaModel.lockDown(true); // Calculate validation requirements this.pclasses.forEach(PermazenClass::calculateValidationRequirement); // Determine if any PermazenClass requires JSR 303 validation, and if so find some representative annotation final AnnotatedElement elementRequiringJSR303Validation = this.pclasses.stream() .map(pclass -> pclass.elementRequiringJSR303Validation) .filter(Objects::nonNull) .findFirst() .orElse(null); // Initialize ValidatorFactory (only if needed) ValidatorFactory optionalValidatorFactory = config.getValidatorFactory(); if (optionalValidatorFactory == null && elementRequiringJSR303Validation != null) { try { optionalValidatorFactory = Validation.buildDefaultValidatorFactory(); } catch (Exception e) { try { final MessageInterpolator messageInterpolator = (MessageInterpolator)Class.forName( HIBERNATE_PARAMETER_MESSAGE_INTERPOLATOR_CLASS_NAME, false, Thread.currentThread().getContextClassLoader()) .getConstructor() .newInstance(); optionalValidatorFactory = Validation.byDefaultProvider() .configure() .messageInterpolator(messageInterpolator) .buildValidatorFactory(); } catch (Exception e2) { this.log.info("loader = {}", this.loader.getParent()); this.log.info("loader.urls = {}", java.util.Arrays.asList(((ApplicationClassLoader)this.loader.getParent()).getURLs())); throw new PermazenException(String.format( "JSR 303 validation constraint found on %s but creation of default ValidatorFactory failed;" + " is there a JSR 303 validation implementation on the classpath?", elementRequiringJSR303Validation), e2); } } } this.validatorFactory = optionalValidatorFactory; // Auto-initialize? if (config.isInitializeOnCreation()) this.initialize(); } finally { Thread.currentThread().setContextClassLoader(prevLoader); } } // Initialization /** * Determine whether this instance is {@linkplain #initialize initialized}. * * @return true if initialized, otherwise false */ public synchronized boolean isInitialized() { return this.initialized; } /** * Initialize this instance if needed. * * @return true if this instance was actually initialized, false if it was already initialized and so nothing happened * @throws InvalidSchemaException if the data model schema conflicts with what's registered in the database */ public synchronized boolean initialize() { // Already initialized? if (this.initialized) return false; // Set "initialized" flag but unset if initialization fails boolean success = false; this.initialized = true; try { this.doInitialize(); success = true; } finally { if (!success) this.initialized = false; } // Done return true; } private void doInitialize() { // Connect to database and register schema final Transaction tx = this.db.createTransaction(this.buildTransactionConfig(null)); final Schema schema; final SchemaBundle schemaBundle; try { schema = tx.getSchema(); schemaBundle = tx.getSchemaBundle(); tx.commit(); } finally { tx.rollback(); // does nothing if transaction succeeded } // Copy storage ID assignments into the corresponding SchemaItem's and PermazenSchemaItem's this.pclasses.forEach(pclass -> pclass.visitSchemaItems(item -> { final SchemaItem schemaItem = (SchemaItem)item.schemaItem; final SchemaId schemaId = schemaItem.getSchemaId(); final int storageId = schemaBundle.getStorageId(schemaId); schemaItem.setStorageId(storageId); item.storageId = storageId; })); this.schemaModel.lockDown(true); this.schemaModel.validate(); // should always be valid, but just to be sure if (this.log.isTraceEnabled()) this.log.trace("Permazen schema with storage ID assignments:\n{}", this.schemaModel); // Populate PermazenClass and PermazenField maps keyed by storage ID this.pclasses.forEach(pclass -> this.pclassesByStorageId.put(pclass.storageId, pclass)); this.pclasses.forEach(pclass -> pclass.fieldsByName.values().forEach( pfield -> pclass.fieldsByStorageId.put(pfield.storageId, pfield))); this.pclasses.forEach(pclass -> pclass.simpleFieldsByName.values().forEach( pfield -> pclass.simpleFieldsByStorageId.put(pfield.storageId, pfield))); // Update all PermazenSchemaItem's to point to core API SchemaItem instead of SchemaModel SchemaItem this.pclasses.forEach(pclass -> pclass.replaceSchemaItems(schema)); // Find all fields that require default validation for (PermazenClass pclass : this.pclasses) { for (PermazenField pfield : pclass.fieldsByName.values()) { if (pfield.requiresDefaultValidation) this.fieldsRequiringDefaultValidation.add(pfield.storageId); } } // Populate this.indexesByStorageId this.pclasses.forEach(pclass -> { // Add simple field indexes pclass.simpleFieldsByName.values().forEach(pfield -> { if (pfield.indexed) this.indexesByStorageId.put(pfield.storageId, pfield); }); // Add composite indexes pclass.jcompositeIndexesByName.values().forEach(index -> this.indexesByStorageId.put(index.storageId, index)); }); // Populate this.typeFieldMap this.pclasses.forEach(pclass -> pclass.fieldsByName.values().forEach(pfield -> { this.typeFieldMap.put(new Tuple2<>(pclass.storageId, pfield.name), pfield); if (pfield instanceof PermazenComplexField) { final PermazenComplexField parentField = (PermazenComplexField)pfield; for (PermazenSimpleField subField : parentField.getSubFields()) this.typeFieldMap.put(new Tuple2<>(pclass.storageId, subField.getFullName()), subField); } })); // Populate pclass forwardCascadeMap and inverseCascadeMap this.pclasses.forEach(pclass -> pclass.simpleFieldsByName.values().forEach(pfield0 -> { // Filter for reference fields if (!(pfield0 instanceof PermazenReferenceField)) return; final PermazenReferenceField pfield = (PermazenReferenceField)pfield0; // Do forward cascades for (String cascadeName : pfield.forwardCascades) pclass.forwardCascadeMap.computeIfAbsent(cascadeName, s -> new ArrayList<>()).add(pfield); // Do inverse cascades for (String cascadeName : pfield.inverseCascades) { for (PermazenClass refPClass : this.getPermazenClasses(pfield.typeToken.getRawType())) { refPClass.inverseCascadeMap .computeIfAbsent(cascadeName, s -> new HashMap<>()) .computeIfAbsent(pfield.storageId, i -> new KeyRanges()) .add(ObjId.getKeyRange(pclass.storageId)); } } })); // Scan for various method-level annotations this.pclasses.forEach(PermazenClass::scanAnnotations); // Eagerly load all generated Java classes so we "fail fast" if there are any loading errors this.untypedClassGenerator.generateClass(); for (PermazenClass pclass : this.pclasses) pclass.getClassGenerator().generateClass(); } // Accessors /** * Get the core API {@link Database} underlying this instance. * * @return underlying {@link Database} */ public Database getDatabase() { return this.db; } // Transactions /** * Open a new transaction. * *

* Convenience method; equivalent to: *

     *  {@link #createTransaction(ValidationMode, Map) createTransaction}({@link ValidationMode#AUTOMATIC}, null)
     *  
* * @return the newly created transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database */ public PermazenTransaction createTransaction() { return this.createTransaction(ValidationMode.AUTOMATIC, null); } /** * Open a new transaction using the specified {@link ValidationMode}. * *

* Convenience method; equivalent to: *

     *  {@link #createTransaction(ValidationMode, Map) createTransaction}(validationMode, null)
     *  
* * @param validationMode the {@link ValidationMode} to use for the new transaction * @return the newly created transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code validationMode} is null */ public PermazenTransaction createTransaction(ValidationMode validationMode) { return this.createTransaction(validationMode, null); } /** * Open a new transaction using the specified {@link ValidationMode} and key/value transaction options. * *

* This does not invoke {@link PermazenTransaction#setCurrent PermazenTransaction.setCurrent()}; * the caller is responsible for doing that if/when needed. * * @param validationMode the {@link ValidationMode} to use for the new transaction * @param kvoptions {@link KVDatabase}-specific transaction options, or null for none * @return the newly created transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code validationMode} is null */ public PermazenTransaction createTransaction(ValidationMode validationMode, Map kvoptions) { Preconditions.checkArgument(validationMode != null, "null validationMode"); this.initialize(); return this.createTransaction(this.db.createTransaction(this.buildTransactionConfig(kvoptions)), validationMode); } /** * Open a new branched transaction. * *

* Convenience method; equivalent to: *

     *  {@link #createBranchedTransaction(ValidationMode, Map, Map)
     *      createBranchedTransaction}({@link ValidationMode#AUTOMATIC}, null, null)
     *  
* * @return the newly created branched transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws UnsupportedOperationException if the key/value database doesn't support {@link KVTransaction#readOnlySnapshot} */ public PermazenTransaction createBranchedTransaction() { return this.createBranchedTransaction(ValidationMode.AUTOMATIC, null, null); } /** * Open a new branched transaction using the specified {@link ValidationMode}. * *

* Convenience method; equivalent to: *

     *  {@link #createBranchedTransaction(ValidationMode, Map, Map) createBranchedTransaction}(validationMode, null, null)
     *  
* * @return the newly created branched transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws UnsupportedOperationException if the key/value database doesn't support {@link KVTransaction#readOnlySnapshot} */ public PermazenTransaction createBranchedTransaction(ValidationMode validationMode) { return this.createBranchedTransaction(validationMode, null, null); } /** * Open a new branched transaction using the specified {@link ValidationMode} and key/value transaction options. * *

* This does not invoke {@link PermazenTransaction#setCurrent PermazenTransaction.setCurrent()}; * the caller is responsible for doing that if/when needed. * * @param validationMode the {@link ValidationMode} to use for the new transaction * @param openOptions {@link KVDatabase}-specific transaction options for the branch's opening transaction, or null for none * @param syncOptions {@link KVDatabase}-specific transaction options for the branch's commit transaction, or null for none * @return the newly created branched transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws UnsupportedOperationException if the key/value database doesn't support {@link KVTransaction#readOnlySnapshot} * @throws IllegalArgumentException if {@code validationMode} is null */ public PermazenTransaction createBranchedTransaction(ValidationMode validationMode, Map openOptions, Map syncOptions) { Preconditions.checkArgument(validationMode != null, "null validationMode"); this.initialize(); final BranchedKVTransaction kvt = new BranchedKVTransaction(this.db.getKVDatabase(), openOptions, syncOptions); Transaction tx = null; try { kvt.open(); tx = this.db.createTransaction(kvt, this.buildTransactionConfig(openOptions)); } finally { if (tx == null) { try { kvt.rollback(); } catch (KVTransactionException e) { // ignore } } } return this.createTransaction(tx, validationMode); } /** * Create a new {@link PermazenTransaction} using an already-opened {@link KVTransaction}. * *

* This does not invoke {@link PermazenTransaction#setCurrent PermazenTransaction.setCurrent()}; * the caller is responsible for doing that if/when needed. * * @param kvt already opened key/value store transaction * @param validationMode the {@link ValidationMode} to use for the new transaction * @return the newly created transaction * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code kvt} or {@code validationMode} is null */ public PermazenTransaction createTransaction(KVTransaction kvt, ValidationMode validationMode) { Preconditions.checkArgument(validationMode != null, "null validationMode"); this.initialize(); return this.createTransaction(this.db.createTransaction(kvt, this.buildTransactionConfig(null)), validationMode); } private PermazenTransaction createTransaction(Transaction tx, ValidationMode validationMode) { assert tx != null; assert validationMode != null; synchronized (this) { assert this.initialized; } return new PermazenTransaction(this, tx, validationMode); } /** * Create a new, empty {@link DetachedPermazenTransaction} backed by a {@link MemoryKVStore}. * *

* The returned {@link DetachedPermazenTransaction} does not support {@link DetachedPermazenTransaction#commit commit()} or * {@link DetachedPermazenTransaction#rollback rollback()}, and can be used indefinitely. * * @param validationMode the {@link ValidationMode} to use for the detached transaction * @return initially empty detached transaction */ public DetachedPermazenTransaction createDetachedTransaction(ValidationMode validationMode) { return this.createDetachedTransaction(new MemoryKVStore(), validationMode); } /** * Create a new {@link DetachedPermazenTransaction} based on the provided key/value store. * *

* The key/value store will be initialized if necessary (i.e., {@code kvstore} may be empty), otherwise it will be * validated against the schema information associated with this instance. * *

* The returned {@link DetachedPermazenTransaction} does not support {@link DetachedPermazenTransaction#commit commit()} or * {@link DetachedPermazenTransaction#rollback rollback()}, and can be used indefinitely. * *

* If {@code kvstore} is a {@link CloseableKVStore}, then it will be {@link CloseableKVStore#close close()}'d * if/when the returned {@link DetachedPermazenTransaction} is. * * @param kvstore key/value store, empty or having content compatible with this transaction's {@link Permazen} * @param validationMode the {@link ValidationMode} to use for the detached transaction * @return detached transaction based on {@code kvstore} * @throws io.permazen.core.SchemaMismatchException if {@code kvstore} contains incompatible or missing schema information * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code kvstore} or {@code validationMode} is null */ public DetachedPermazenTransaction createDetachedTransaction(KVStore kvstore, ValidationMode validationMode) { Preconditions.checkArgument(validationMode != null, "null validationMode"); this.initialize(); final DetachedTransaction dtx = this.db.createDetachedTransaction(kvstore, this.buildTransactionConfig(null)); return new DetachedPermazenTransaction(this, dtx, validationMode); } /** * Build the {@link TransactionConfig} for a new core API transaction. * * @return core API transaction config * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ protected TransactionConfig buildTransactionConfig(Map kvoptions) { // Protect schema model while it's not yet locked down SchemaModel txModel = this.schemaModel; if (!txModel.isLockedDown(true)) txModel = txModel.clone(); // Build config return TransactionConfig.builder() .schemaModel(txModel) .allowNewSchema(true) .schemaRemoval(TransactionConfig.SchemaRemoval.CONFIG_CHANGE) .kvOptions(kvoptions) .build(); } // Schema /** * Get the {@link SchemaModel} associated with this instance derived from the annotations on the scanned classes, * including actual storage ID assignments. * *

* Equivalent to: {@link #getSchemaModel(boolean) getSchemaModel}{@code (true)}. * * @return schema model used by this database * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public SchemaModel getSchemaModel() { return this.getSchemaModel(true); } /** * Get the {@link SchemaModel} associated with this instance derived from the annotations on the scanned classes. * * @param withStorageIds true to include actual storage ID assignments * @return schema model used by this database * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public SchemaModel getSchemaModel(boolean withStorageIds) { if (withStorageIds) this.initialize(); return withStorageIds ? this.schemaModel : this.origSchemaModel; } // PermazenClass access /** * Get all {@link PermazenClass}'s associated with this instance, indexed by object type name. * * @return read-only mapping from object type name to {@link PermazenClass} * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public NavigableMap> getPermazenClassesByName() { this.initialize(); return Collections.unmodifiableNavigableMap(this.pclassesByName); } /** * Get the {@link PermazenClass} associated with the given object type name. * * @param typeName object type name * @return {@link PermazenClass} instance * @throws UnknownTypeException if {@code typeName} is unknown * @throws IllegalArgumentException if {@code typeName} is null * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public PermazenClass getPermazenClass(String typeName) { this.initialize(); final PermazenClass pclass = this.pclassesByName.get(typeName); if (pclass == null) throw new UnknownTypeException(typeName, null); return pclass; } /** * Get all {@link PermazenClass}'s associated with this instance, indexed by storage ID. * * @return read-only mapping from storage ID to {@link PermazenClass} * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public NavigableMap> getPermazenClassesByStorageId() { this.initialize(); return Collections.unmodifiableNavigableMap(this.pclassesByStorageId); } /** * Get all {@link PermazenClass}'s associated with this instance, indexed by Java model type. * * @return read-only mapping from Java model type to {@link PermazenClass} * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public Map, PermazenClass> getPermazenClassesByType() { this.initialize(); return Collections.unmodifiableMap(this.pclassesByType); } /** * Get the {@link PermazenClass} modeled by the given type. * * @param type an annotated Java object model type * @param Java model type * @return associated {@link PermazenClass} * @throws IllegalArgumentException if {@code type} is not equal to a known Java object model type * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ @SuppressWarnings("unchecked") public PermazenClass getPermazenClass(Class type) { this.initialize(); final PermazenClass pclass = this.pclassesByType.get(type); if (pclass == null) throw new IllegalArgumentException(String.format("java model type is not recognized: %s", type)); return (PermazenClass)pclass; } /** * Find the most specific {@link PermazenClass} for which the give type is a sub-type of the corresponding Java model type. * * @param type (sub)type of some Java object model type * @param Java model type or subtype thereof * @return narrowest {@link PermazenClass} whose Java object model type is a supertype of {@code type}, or null if none found * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ @SuppressWarnings("unchecked") public PermazenClass findPermazenClass(Class type) { this.initialize(); for (Class superType = type; superType != null; superType = superType.getSuperclass()) { final PermazenClass pclass = this.pclassesByType.get(superType); if (pclass != null) return (PermazenClass)pclass; } return null; } /** * Get the {@link PermazenClass} associated with the object ID. * * @param id object ID * @return {@link PermazenClass} instance * @throws UnknownTypeException if {@code id} has a type that is not defined in this instance's schema * @throws IllegalArgumentException if {@code id} is null * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public PermazenClass getPermazenClass(ObjId id) { Preconditions.checkArgument(id != null, "null id"); return this.getPermazenClass(id.getStorageId()); } /** * Get the {@link PermazenClass} associated with the given storage ID. * * @param storageId object type storage ID * @return {@link PermazenClass} instance * @throws UnknownTypeException if {@code storageId} does not represent an object type * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ public PermazenClass getPermazenClass(int storageId) { this.initialize(); final PermazenClass pclass = this.pclassesByStorageId.get(storageId); if (pclass == null) throw new UnknownTypeException(String.format("storage ID %d", storageId), null); return pclass; } /** * Get all {@link PermazenClass}es which sub-type the given type. * * @param type type restriction, or null for no restrction * @param Java model type * @return list of {@link PermazenClass}es whose type is {@code type} or a sub-type, ordered by storage ID * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails */ @SuppressWarnings("unchecked") public List> getPermazenClasses(Class type) { this.initialize(); return this.pclasses.stream() .filter(pclass -> type == null || type.isAssignableFrom(pclass.type)) .map(pclass -> (PermazenClass)pclass) .collect(Collectors.toList()); } /** * Quick lookup for the {@link PermazenField} corresponding to the given object and field name. * * @param id object ID * @param fieldName field name; sub-fields of complex fields may be specified like {@code "mymap.key"} * @param expected encoding * @throws UnknownTypeException if {@code id} has a type that is not defined in this instance's schema * @throws UnknownFieldException if {@code fieldName} does not correspond to any field in the object's type * @throws IllegalArgumentException if any parameter is null */ @SuppressWarnings("unchecked") T getField(ObjId id, String fieldName, Class type) { Preconditions.checkArgument(id != null, "null id"); Preconditions.checkArgument(fieldName != null, "null fieldName"); Preconditions.checkArgument(type != null, "null type"); final PermazenField pfield = this.typeFieldMap.get(new Tuple2<>(id.getStorageId(), fieldName)); if (pfield == null) { this.getPermazenClass(id.getStorageId()).getField(fieldName, type); // should always throw the appropriate exception assert false; } try { return type.cast(pfield); } catch (ClassCastException e) { throw new UnknownFieldException(fieldName, String.format( "%s is not a %s field", pfield, type.getSimpleName().replaceAll("^J(.*)Field$", "").toLowerCase())); } } /** * Generate the {@link KeyRanges} restricting objects to the specified type. * * @param type any Java type, or null for no restriction * @return key restriction for {@code type} */ KeyRanges keyRangesFor(Class type) { if (type == null) return KeyRanges.full(); final ArrayList list = new ArrayList<>(this.pclasses.size()); boolean invert = false; if (type == UntypedPermazenObject.class) { type = null; invert = true; } this.getPermazenClasses(type).stream() .map(pclass -> ObjId.getKeyRange(pclass.storageId)) .iterator() .forEachRemaining(list::add); final KeyRanges keyRanges = new KeyRanges(list); return invert ? keyRanges.inverse() : keyRanges; } // Reference Paths /** * Parse a {@link ReferencePath} starting from a Java type. * *

* Roughly equivalent to: {@code this.parseReferencePath(this.getPermazenClasses(startType), path)}. * * @param startType starting Java type for the path * @param path reference path in string form * @return parsed reference path * @throws IllegalArgumentException if no model types are instances of {@code startType} * @throws IllegalArgumentException if {@code path} is invalid * @throws IllegalArgumentException if either parameter is null * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails * @see ReferencePath */ public ReferencePath parseReferencePath(Class startType, String path) { Preconditions.checkArgument(startType != null, "null startType"); final HashSet> startTypes = new HashSet<>(this.getPermazenClasses(startType)); if (startTypes.isEmpty()) throw new IllegalArgumentException(String.format("no model type is an instance of %s", startType)); if (startType.isAssignableFrom(UntypedPermazenObject.class)) startTypes.add(null); return this.parseReferencePath(startTypes, path); } /** * Parse a {@link ReferencePath} starting from a set of model object types. * * @param startTypes starting model types for the path, with null meaning {@link UntypedPermazenObject} * @param path reference path in string form * @return parsed reference path * @throws IllegalArgumentException if {@code startTypes} is empty or contains null * @throws IllegalArgumentException if {@code path} is invalid * @throws IllegalArgumentException if either parameter is null * @throws InvalidSchemaException if this instance is not yet {@link #initialize initialized} and schema registration fails * @see ReferencePath */ public ReferencePath parseReferencePath(Set> startTypes, String path) { return this.referencePathCache.get(startTypes, path); } // Misc utility // IndexQuery Cache IndexQuery getIndexQuery(IndexQuery.Key key) { try { return this.indexQueryCache.getUnchecked(key); } catch (UncheckedExecutionException e) { Throwables.throwIfUnchecked(e.getCause()); throw e; } } // Internal Stuff // Get class generator for "untyped" PermazenObject's ClassGenerator getUntypedClassGenerator() { return this.untypedClassGenerator; } // Loader private class Loader extends ClassLoader { static { ClassLoader.registerAsParallelCapable(); } // Set up class loader Loader() { super(ApplicationClassLoader.getInstance()); } // Find matching ClassGenerator, if any, otherwise defer to parent @Override protected Class findClass(String name) throws ClassNotFoundException { for (ClassGenerator generator : Permazen.this.classGenerators) { if (name.equals(generator.getClassName().replace('/', '.'))) { final byte[] bytes = generator.generateBytecode(); return this.defineClass(name, bytes, 0, bytes.length); } } return super.findClass(name); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy