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

io.permazen.Permazen Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show 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.collect.Iterables;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.UncheckedExecutionException;

import io.permazen.annotation.JCompositeIndexes;
import io.permazen.annotation.PermazenType;
import io.permazen.core.Database;
import io.permazen.core.ObjId;
import io.permazen.core.SnapshotTransaction;
import io.permazen.core.Transaction;
import io.permazen.core.TypeNotInSchemaVersionException;
import io.permazen.core.UnknownFieldException;
import io.permazen.core.UnknownTypeException;
import io.permazen.core.type.ReferenceFieldType;
import io.permazen.kv.KVStore;
import io.permazen.kv.KVTransaction;
import io.permazen.kv.KeyRange;
import io.permazen.kv.KeyRanges;
import io.permazen.kv.simple.SimpleKVDatabase;
import io.permazen.kv.util.NavigableMapKVStore;
import io.permazen.schema.NameIndex;
import io.permazen.schema.SchemaModel;
import io.permazen.schema.SchemaObjectType;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;

import javax.validation.Validation;
import javax.validation.ValidatorFactory;

import org.dellroad.stuff.util.LongMap;
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 layer is a simple {@code byte[]} key/value database represented by the * {@link io.permazen.kv.KVDatabase} class. Transactions are supported at this layer and are accessed * through the {@link io.permazen.kv.KVTransaction} interface. * There are several available {@link io.permazen.kv.KVDatabase} implementations, including "wrappers" * for several third party key/value stores.
  • *
  • On top of that sits the core API layer, which provides a rigourous database abstraction on top of the * key/value store. It supports simple fields of any atomic Java type, as well as list, set, and map complex fields, * tightly controlled schema versioning, simple and composite indexes, and lifecycle and change notifications. * It is not Java-specific or explicitly object-oriented. The core API is accessed through the {@link Database} * and {@link io.permazen.core.Transaction} classes.
  • *
  • The top layer is a Java-centric, type safe, object-oriented persistence layer for Java applications. * It sits on top of the core API layer and provides a fully type-safe Java view of a core API {@link Transaction}, * where all access is through user-supplied Java model classes. Database types and fields, and Java listener methods * are all declared using {@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 JTransaction} * represents the corresonding transactions.
  • *
* *

* User-provided Java model classes define database fields by declaring abstract Java bean property methods. * {@link Permazen} generates concrete subclasses of the user-provided abstract model classes at runtime. * These runtime classes implement the abstract bean property methods, as well as the {@link JObject} interface. * Java model class instances are always associated with a specific {@link JTransaction}, and all of their database * state derives from that the underlying key/value {@link io.permazen.kv.KVTransaction}. * *

* All Java model class instances have a unique {@link ObjId} which represents database identity. {@link Permazen} * guarantees that at most one Java model class instance instance will exist for any given {@link JTransaction} and {@link ObjId}. * Instance creation, index queries, and certain other database-related tasks are initiated using a {@link JTransaction}. * *

* Normal database transactions are created via {@link #createTransaction createTransaction()}. "Snapshot" transactions are * purely in-memory transactions that are detached from the database and may persist indefinitely. Their purpose is to hold a * snapshot of some (user-defined) portion of the database content for use outside of a regular transaction. Otherwise, * they function like normal transactions, with support for index queries, listener callbacks, etc. See * {@link JTransaction#createSnapshotTransaction JTransaction.createSnapshotTransaction()}, * {@link JTransaction#getSnapshotTransaction}, {@link JObject#copyOut JObject.copyOut()}, and * {@link JObject#copyIn JObject.copyIn()}. * *

* Instances of this class are usually created using a {@link PermazenFactory}. * * @see JObject * @see JTransaction * @see PermazenFactory * @see io.permazen.annotation */ 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"; final Logger log = LoggerFactory.getLogger(this.getClass()); final TreeMap> jclasses = new TreeMap<>(); final HashMap, JClass> jclassesByType = new HashMap<>(); final HashMap indexInfoMap = new HashMap<>(); final LongMap typeFieldMap = new LongMap<>(); final HashSet fieldsRequiringDefaultValidation = new HashSet<>(); final ReferencePathCache referencePathCache = new ReferencePathCache(this); final ClassGenerator untypedClassGenerator; final ArrayList> classGenerators; final ClassLoader loader; final Database db; final StorageIdGenerator storageIdGenerator; final boolean hasOnCreateMethods; final boolean hasOnDeleteMethods; final boolean hasOnVersionChangeMethods; final boolean hasUpgradeConversions; final boolean anyJClassRequiresDefaultValidation; final AnnotatedElement elementRequiringJSR303Validation; // Cached listener sets used by JTransaction.() final Transaction.ListenerSet[] listenerSets = new Transaction.ListenerSet[4]; ValidatorFactory validatorFactory; volatile int configuredVersion; volatile int actualVersion; private final LoadingCache indexQueryInfoCache = CacheBuilder.newBuilder() .maximumSize(1000).build(new CacheLoader() { @Override public IndexQueryInfo load(IndexQueryInfoKey key) { return key.getIndexQueryInfo(Permazen.this); } }); private SchemaModel schemaModel; private NameIndex nameIndex; // Constructors /** * Create an instance using an initially empty, in-memory {@link SimpleKVDatabase}. * Generates a database schema by introspecting the {@code classes}; auto-generates a schema version number * and uses a {@link DefaultStorageIdGenerator} to auto-generate storage ID's. * *

* This constructor can also be used just to validate the annotations on the given classes. * * @param classes classes annotated with {@link PermazenType @PermazenType} annotations * @throws IllegalArgumentException if {@code classes} is null * @throws IllegalArgumentException if {@code classes} contains a null class or a class with invalid annotation(s) * @throws IllegalArgumentException if {@code classes} contains a class with no suitable subclass constructor * @throws io.permazen.core.InvalidSchemaException if the schema implied by {@code classes} is invalid */ public Permazen(Iterable> classes) { this(new Database(new SimpleKVDatabase()), -1, new DefaultStorageIdGenerator(), classes); } /** * Create an instance using an initially empty, in-memory {@link SimpleKVDatabase}. * *

* Equivalent to {@link #Permazen(Iterable) Permazen}{@code (Arrays.asList(classes))}. * * @param classes classes annotated with {@link PermazenType @PermazenType} annotations * @see #Permazen(Iterable) */ public Permazen(Class... classes) { this(Arrays.asList(classes)); } /** * Primary constructor. * * @param database core database to use * @param version schema version number of the schema derived from {@code classes}, * zero to use the highest version already recorded in the database, * or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version * @param storageIdGenerator generator for auto-generated storage ID's, or null to disallow auto-generation of storage ID's * @param classes classes annotated with {@link PermazenType @PermazenType} annotations; non-annotated classes are ignored * @throws IllegalArgumentException if {@code database} or {@code classes} is null * @throws IllegalArgumentException if {@code version} is less than -1 * @throws IllegalArgumentException if {@code classes} contains a null class or a class with invalid annotation(s) * @throws io.permazen.core.InvalidSchemaException if the schema implied by {@code classes} is invalid */ public Permazen(Database database, int version, StorageIdGenerator storageIdGenerator, Iterable> classes) { // Initialize Preconditions.checkArgument(database != null, "null database"); Preconditions.checkArgument(version >= -1, "invalid schema version"); Preconditions.checkArgument(classes != null, "null classes"); this.db = database; this.storageIdGenerator = storageIdGenerator; this.loader = AccessController.doPrivileged(new PrivilegedAction() { @Override public Loader run() { return Permazen.this.new Loader(); } }); // Inventory classes; automatically add all @PermazenType-annotated superclasses of @PermazenType-annotated classes final HashSet> permazenTypes = new HashSet<>(); for (Class type : classes) { // Sanity check Preconditions.checkArgument(type != null, "null class found in classes"); // Add type and all @PermazenType-annotated superclasses do { // Find annotation final PermazenType annotation = Util.getAnnotation(type, PermazenType.class); if (annotation == null) continue; // Sanity check type if (type.isPrimitive() || type.isArray()) { throw new IllegalArgumentException("illegal type " + type + " for @" + PermazenType.class.getSimpleName() + " annotation: not a normal class or interface"); } // Add class permazenTypes.add(type); } while ((type = type.getSuperclass()) != null); } // Add Java model classes for (Class type : permazenTypes) { // Get annotation final PermazenType annotation = Util.getAnnotation(type, PermazenType.class); final String name = annotation.name().length() != 0 ? annotation.name() : type.getSimpleName(); if (this.log.isTraceEnabled()) { this.log.trace("found @" + PermazenType.class.getSimpleName() + " annotation on " + type + " defining object type `" + name + "'"); } // Get storage ID int storageId = annotation.storageId(); if (storageId == 0) storageId = this.getStorageIdGenerator(annotation, type).generateClassStorageId(type, name); // Create JClass JClass jclass; try { jclass = this.createJClass(name, storageId, type); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("invalid @" + PermazenType.class.getSimpleName() + " annotation on " + type + ": " + e, e); } // Add jclass this.addJClass(jclass); this.log.debug("added Java model class `" + jclass.name + "' with storage ID " + jclass.storageId); } // Inventory class generators this.classGenerators = this.jclasses.values().stream() .map(jclass -> jclass.classGenerator) .collect(Collectors.toCollection(ArrayList::new)); this.untypedClassGenerator = new ClassGenerator<>(this, UntypedJObject.class); this.classGenerators.add(this.untypedClassGenerator); // Create fields for (JClass jclass : this.jclasses.values()) jclass.createFields(this); // Add composite indexes to class; like fields, indexes are inherited (duplicated) from supertypes for (JClass jclass : this.jclasses.values()) { for (Class supertype : TypeToken.of(jclass.type).getTypes().rawTypes()) { final io.permazen.annotation.JCompositeIndex[] annotations; final JCompositeIndexes container = Util.getAnnotation(supertype, JCompositeIndexes.class); if (container != null) annotations = container.value(); else { io.permazen.annotation.JCompositeIndex annotation = Util.getAnnotation(supertype, io.permazen.annotation.JCompositeIndex.class); if (annotation == null) continue; annotations = new io.permazen.annotation.JCompositeIndex[] { annotation }; } for (io.permazen.annotation.JCompositeIndex annotation : annotations) { if (annotation.uniqueExclude().length > 0 && !annotation.unique()) { throw new IllegalArgumentException("invalid @JCompositeIndex annotation on " + supertype + ": use of uniqueExclude() requires unique = true"); } jclass.addCompositeIndex(this, supertype, annotation); } } } // Find all fields that require default validation for (JClass jclass : this.jclasses.values()) { for (JField jfield : jclass.jfields.values()) { if (jfield.requiresDefaultValidation) this.fieldsRequiringDefaultValidation.add(jfield.storageId); } } // Populate this.indexInfoMap final Map descriptionMap = new HashMap<>(); for (JClass jclass : this.jclasses.values()) { // Find simple field indexes for (JField jfield : jclass.jfields.values()) { if (jfield instanceof JSimpleField) { final JSimpleField simpleField = (JSimpleField)jfield; if (simpleField.indexed) this.addIndexInfo(simpleField, descriptionMap); } else if (jfield instanceof JComplexField) { final JComplexField parentField = (JComplexField)jfield; for (JSimpleField subField : parentField.getSubFields()) { if (subField.indexed) this.addIndexInfo(subField, descriptionMap); } } } // Find composite indexes for (JCompositeIndex index : jclass.jcompositeIndexes.values()) this.addIndexInfo(index, descriptionMap); } // Populate this.typeFieldMap for (JClass jclass : this.jclasses.values()) { for (JField jfield : jclass.jfields.values()) { this.typeFieldMap.put(this.getTypeFieldKey(jclass.storageId, jfield.storageId), jfield); if (jfield instanceof JComplexField) { final JComplexField parentField = (JComplexField)jfield; for (JSimpleField subField : parentField.getSubFields()) this.typeFieldMap.put(this.getTypeFieldKey(jclass.storageId, subField.storageId), subField); } } } // Populate jclass forwardCascadeMap and inverseCascadeMap for (JClass jclass0 : this.jclasses.values()) { final JClass jclass = jclass0; for (JField jfield : jclass.jfields.values()) { jfield.visit(new JFieldSwitchAdapter() { @Override public Void caseJReferenceField(JReferenceField field) { // Do forward cascades for (String cascadeName : field.forwardCascades) { if (cascadeName == null) continue; jclass.forwardCascadeMap.computeIfAbsent(cascadeName, s -> new ArrayList<>()).add(field); } // Do inverse cascades for (String cascadeName : field.inverseCascades) { if (cascadeName == null) continue; for (JClass targetClass : Permazen.this.getJClasses(field.typeToken.getRawType())) { targetClass.inverseCascadeMap .computeIfAbsent(cascadeName, s -> new HashMap<>()) .computeIfAbsent(field.storageId, i -> new KeyRanges()) .add(ObjId.getKeyRange(jclass.storageId)); } } return null; } @Override public Void caseJMapField(JMapField field) { if (field.getKeyField() instanceof JReferenceField) this.caseJReferenceField((JReferenceField)field.getKeyField()); if (field.getValueField() instanceof JReferenceField) this.caseJReferenceField((JReferenceField)field.getValueField()); return null; } @Override protected Void caseJCollectionField(JCollectionField field) { if (field.getElementField() instanceof JReferenceField) this.caseJReferenceField((JReferenceField)field.getElementField()); return null; } @Override protected Void caseJField(JField field) { return null; } }); } } // Scan for other method-level annotations this.jclasses.values() .forEach(JClass::scanAnnotations); // Determine which JClass's have validation requirement(s) on creation this.jclasses.values() .forEach(JClass::calculateValidationRequirement); boolean anyDefaultValidation = false; AnnotatedElement someElementRequiringJSR303Validation = null; for (JClass jclass : this.jclasses.values()) { anyDefaultValidation |= jclass.requiresDefaultValidation; if (someElementRequiringJSR303Validation == null) someElementRequiringJSR303Validation = jclass.elementRequiringJSR303Validation; } this.anyJClassRequiresDefaultValidation = anyDefaultValidation; this.elementRequiringJSR303Validation = someElementRequiringJSR303Validation; // Detect whether we have any @OnCreate, @OnDelete, and/or @OnVersionChange methods boolean anyOnCreateMethods = false; boolean anyOnDeleteMethods = false; boolean anyOnVersionChangeMethods = false; boolean anyUpgradeConversions = false; for (JClass jclass : this.jclasses.values()) { anyOnCreateMethods |= !jclass.onCreateMethods.isEmpty(); anyOnDeleteMethods |= !jclass.onDeleteMethods.isEmpty(); anyOnVersionChangeMethods |= !jclass.onVersionChangeMethods.isEmpty(); anyUpgradeConversions |= !jclass.upgradeConversionFields.isEmpty(); } this.hasOnCreateMethods = anyOnCreateMethods; this.hasOnDeleteMethods = anyOnDeleteMethods; this.hasOnVersionChangeMethods = anyOnVersionChangeMethods; this.hasUpgradeConversions = anyUpgradeConversions; // Validate schema this.db.validateSchema(this.getSchemaModel()); // Auto-generate schema version if requested this.configuredVersion = version == -1 ? this.schemaModel.autogenerateVersion() : version; // Eagerly load all generated Java classes so we "fail fast" if there are any loading errors this.untypedClassGenerator.generateClass(); for (JClass jclass : this.jclasses.values()) jclass.getClassGenerator().generateClass(); } // This method exists solely to bind the generic type parameters private JClass createJClass(String name, int storageId, Class type) { return new JClass<>(this, name, storageId, type); } StorageIdGenerator getStorageIdGenerator(Annotation annotation, AnnotatedElement target) { if (this.storageIdGenerator == null) { throw new IllegalArgumentException("invalid @" + annotation.annotationType().getSimpleName() + " annotation on " + target + ": no storage ID is given, but storage ID auto-generation is disabled" + " because no " + StorageIdGenerator.class.getSimpleName() + " is configured"); } return this.storageIdGenerator; } // Accessors /** * Get the core API {@link Database} underlying this instance. * * @return underlying {@link Database} */ public Database getDatabase() { return this.db; } /** * Get the schema version that this instance was configured to use. * *

* This will either be a specific non-zero schema version number, or zero, indicating that the highest schema version * found in the database should be used. * *

* If -1 was configured, this will return the actual {@linkplain SchemaModel#autogenerateVersion auto-generated} * schema version. * * @return the schema version that this instance will use when opening transactions via * {@link Database#createTransaction Database.createTransaction()} */ public int getConfiguredVersion() { return this.configuredVersion; } /** * Change the schema version that this instance is configured to use. * * @param version schema version number of the schema derived from {@code classes}, * zero to use the highest version already recorded in the database, * or -1 to use an {@linkplain SchemaModel#autogenerateVersion auto-generated} schema version * @throws IllegalArgumentException if {@code version} is less than -1 */ public void setConfiguredVersion(int version) { Preconditions.checkArgument(version >= -1, "invalid schema version"); this.configuredVersion = version == -1 ? this.schemaModel.autogenerateVersion() : version; } /** * Get the schema version that this instance used for the most recently created transaction. * *

* If no transactions have been created yet, this returns zero. Otherwise, it returns the schema version * used by the most recently created transaction. * *

* If the {@code version} passed to the constructor was zero, this method can be used to read the highest schema * version seen in the database by the most recently created transaction. * *

* If the {@code version} passed to the constructor was non-zero, and at least one transaction has been created, * this method will return the same value. * * @return the schema version that this instance used in the most recently created transaction */ public int getActualVersion() { return this.actualVersion; } /** * Get the Java model class of the given {@link JObject}. * *

* If {@code jobj} is an instance of a Permazen-generated subclass of a user-supplied Java model class, * this returns the original Java model class. * * @param jobj database instance * @return the original Java model class of which {@code jobj} is an instance * @throws IllegalArgumentException if {@code jobj} is null * @deprecated Use {@link JObject#getModelClass} instead */ @Deprecated public static Class getModelClass(JObject jobj) { Preconditions.checkArgument(jobj != null, "null jobj"); return jobj.getModelClass(); } // Transactions /** * Create a new transaction. * *

* Convenience method; equivalent to: *

     *  {@link #createTransaction(boolean, ValidationMode, Map) createTransaction}(true, {@link ValidationMode#AUTOMATIC}, null)
     *  
* * @return the newly created transaction * @throws io.permazen.core.InvalidSchemaException if the schema does not match what's recorded in the * database for the schema version provided to the constructor * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is false * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is true, but the schema is incompatible * with one or more previous schemas alread recorded in the database (i.e., the same storage ID is used * incompatibly between schema versions) * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database */ public JTransaction createTransaction() { return this.createTransaction(true, ValidationMode.AUTOMATIC, null); } /** * Create a new transaction. * *

* Convenience method; equivalent to: *

     *  {@link #createTransaction(boolean, ValidationMode, Map) createTransaction}(allowNewSchema, validationMode, null)
     *  
* * @param allowNewSchema whether creating a new schema version is allowed * @param validationMode the {@link ValidationMode} to use for the new transaction * @return the newly created transaction * @throws io.permazen.core.InvalidSchemaException if the schema does not match what's recorded in the * database for the schema version provided to the constructor * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is false * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is true, but the schema is incompatible * with one or more previous schemas alread recorded in the database (i.e., the same storage ID is used * incompatibly between schema versions) * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code validationMode} is null */ public JTransaction createTransaction(boolean allowNewSchema, ValidationMode validationMode) { return this.createTransaction(allowNewSchema, validationMode, null); } /** * Create a new transaction with key/value transaction options. * *

* This does not invoke {@link JTransaction#setCurrent JTransaction.setCurrent()}: the caller is responsible * for doing that if necessary. However, this method does arrange for * {@link JTransaction#setCurrent JTransaction.setCurrent}{@code (null)} to be invoked as soon as the * returned transaction is committed (or rolled back), assuming {@link JTransaction#getCurrent} returns the * {@link JTransaction} returned here at that time. * * @param allowNewSchema whether creating a new schema version is allowed * @param validationMode the {@link ValidationMode} to use for the new transaction * @param kvoptions optional {@link io.permazen.kv.KVDatabase}-specific transaction options; may be null * @return the newly created transaction * @throws io.permazen.core.InvalidSchemaException if the schema does not match what's recorded in the * database for the schema version provided to the constructor * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is false * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is true, but the schema is incompatible * with one or more previous schemas alread recorded in the database (i.e., the same storage ID is used * incompatibly between schema versions) * @throws io.permazen.core.InconsistentDatabaseException if inconsistent or invalid meta-data is detected in the database * @throws IllegalArgumentException if {@code validationMode} is null */ public JTransaction createTransaction(boolean allowNewSchema, ValidationMode validationMode, Map kvoptions) { return this.createTransaction( this.db.createTransaction(this.getSchemaModel(), this.configuredVersion, allowNewSchema, kvoptions), validationMode); } /** * Create a new transaction using an already-opened {@link KVTransaction}. * *

* This does not invoke {@link JTransaction#setCurrent JTransaction.setCurrent()}: the caller is responsible * for doing that if necessary. However, this method does arrange for * {@link JTransaction#setCurrent JTransaction.setCurrent}{@code (null)} to be invoked as soon as the * returned transaction is committed (or rolled back), assuming {@link JTransaction#getCurrent} returns the * {@link JTransaction} returned here at that time. * * @param kvt already opened key/value store transaction * @param allowNewSchema whether creating a new schema version is allowed * @param validationMode the {@link ValidationMode} to use for the new transaction * @return the newly created transaction * @throws io.permazen.core.InvalidSchemaException if the schema does not match what's recorded in the * database for the schema version provided to the constructor * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is false * @throws io.permazen.core.InvalidSchemaException if the schema version provided to the constructor * is not recorded in the database and {@code allowNewSchema} is true, but the schema is incompatible * with one or more previous schemas alread recorded in the database (i.e., the same storage ID is used * incompatibly between schema versions) * @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 JTransaction createTransaction(KVTransaction kvt, boolean allowNewSchema, ValidationMode validationMode) { return this.createTransaction( this.db.createTransaction(kvt, this.getSchemaModel(), this.configuredVersion, allowNewSchema), validationMode); } private JTransaction createTransaction(Transaction tx, ValidationMode validationMode) { assert tx != null; Preconditions.checkArgument(validationMode != null, "null validationMode"); this.actualVersion = tx.getSchema().getVersionNumber(); final JTransaction jtx = new JTransaction(this, tx, validationMode); tx.addCallback(new CleanupCurrentCallback(jtx)); return jtx; } /** * Create a new, empty {@link SnapshotJTransaction} backed by a {@link NavigableMapKVStore}. * *

* The returned {@link SnapshotJTransaction} does not support {@link SnapshotJTransaction#commit commit()} or * {@link SnapshotJTransaction#rollback rollback()}, and can be used indefinitely. * * @param validationMode the {@link ValidationMode} to use for the snapshot transaction * @return initially empty snapshot transaction */ public SnapshotJTransaction createSnapshotTransaction(ValidationMode validationMode) { return this.createSnapshotTransaction(new NavigableMapKVStore(), true, validationMode); } /** * Create a new {@link SnapshotJTransaction} 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 SnapshotJTransaction} does not support {@link SnapshotJTransaction#commit commit()} or * {@link SnapshotJTransaction#rollback rollback()}, and can be used indefinitely. * * @param kvstore key/value store, empty or having content compatible with this transaction's {@link Permazen} * @param allowNewSchema whether creating a new schema version in {@code kvstore} is allowed * @param validationMode the {@link ValidationMode} to use for the snapshot transaction * @return snapshot 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} is null */ public SnapshotJTransaction createSnapshotTransaction(KVStore kvstore, boolean allowNewSchema, ValidationMode validationMode) { final SnapshotTransaction stx = this.db.createSnapshotTransaction(kvstore, this.getSchemaModel(), this.configuredVersion, allowNewSchema); return new SnapshotJTransaction(this, stx, validationMode); } // Schema /** * Get the {@link SchemaModel} associated with this instance, derived from the annotations on the scanned classes. * * @return the associated schema model */ public SchemaModel getSchemaModel() { if (this.schemaModel == null) { final SchemaModel model = new SchemaModel(); for (JClass jclass : this.jclasses.values()) { final SchemaObjectType schemaObjectType = jclass.toSchemaItem(this); model.getSchemaObjectTypes().put(schemaObjectType.getStorageId(), schemaObjectType); } this.schemaModel = model; this.schemaModel.lockDown(); this.log.debug("Permazen schema generated from annotated classes:\n{}", this.schemaModel); } return this.schemaModel; } /** * Get a {@link NameIndex} based on {@linkplain #getSchemaModel this instance's schema model}. * * @return a name index on this instance's schema model */ public NameIndex getNameIndex() { if (this.nameIndex == null) this.nameIndex = new NameIndex(this.getSchemaModel()); return this.nameIndex; } // JClass access /** * Get all {@link JClass}'s associated with this instance, indexed by storage ID. * * @return read-only mapping from storage ID to {@link JClass} */ public SortedMap> getJClasses() { return Collections.unmodifiableSortedMap(this.jclasses); } /** * Get all {@link JClass}'s associated with this instance, indexed by Java model type. * * @return read-only mapping from Java model type to {@link JClass} */ public Map, JClass> getJClassesByType() { return Collections.unmodifiableMap(this.jclassesByType); } /** * Get the {@link JClass} modeled by the given type. * * @param type an annotated Java object model type * @param Java model type * @return associated {@link JClass} * @throws IllegalArgumentException if {@code type} is not equal to a known Java object model type */ @SuppressWarnings("unchecked") public JClass getJClass(Class type) { final JClass jclass = this.jclassesByType.get(type); if (jclass == null) throw new IllegalArgumentException("java model type is not recognized: " + type); return (JClass)jclass; } /** * Find the most specific {@link JClass} 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 JClass} whose Java object model type is a supertype of {@code type}, or null if none found */ @SuppressWarnings("unchecked") public JClass findJClass(Class type) { for (Class superType = type; superType != null; superType = superType.getSuperclass()) { final JClass jclass = this.jclassesByType.get(superType); if (jclass != null) return (JClass)jclass; } return null; } /** * Get the {@link JClass} associated with the object ID. * * @param id object ID * @return {@link JClass} instance * @throws TypeNotInSchemaVersionException if {@code id} has a type that does not exist in this instance's schema version * @throws IllegalArgumentException if {@code id} is null */ public JClass getJClass(ObjId id) { Preconditions.checkArgument(id != null, "null id"); final JClass jclass = this.jclasses.get(id.getStorageId()); if (jclass == null) throw new TypeNotInSchemaVersionException(id, this.actualVersion); return jclass; } /** * Get the {@link JClass} associated with the given storage ID. * * @param storageId object type storage ID * @return {@link JClass} instance * @throws UnknownTypeException if {@code storageId} does not represent an object type */ public JClass getJClass(int storageId) { final JClass jclass = this.jclasses.get(storageId); if (jclass == null) throw new UnknownTypeException(storageId, this.actualVersion); return jclass; } /** * Get all {@link JClass}es which sub-type the given type. * * @param type type restriction, or null for no restrction * @param Java model type * @return list of {@link JClass}es whose type is {@code type} or a sub-type, ordered by storage ID */ @SuppressWarnings("unchecked") public List> getJClasses(Class type) { return this.jclasses.values().stream() .filter(jclass -> type == null || type.isAssignableFrom(jclass.type)) .map(jclass -> (JClass)jclass) .collect(Collectors.toList()); } /** * Quick lookup for the {@link JField} corresponding to the given object and field storage ID. * * @param id object ID * @param storageId field storage ID * @param expected field type * @return list of {@link JClass}es whose type is {@code type} or a sub-type, ordered by storage ID * @throws TypeNotInSchemaVersionException if {@code id} has a type that does not exist in this instance's schema version * @throws UnknownFieldException if {@code storageId} does not correspond to any field in the object's type */ @SuppressWarnings("unchecked") T getJField(ObjId id, int storageId, Class type) { final JField jfield = this.typeFieldMap.get(this.getTypeFieldKey(id.getStorageId(), storageId)); if (jfield == null) { this.getJClass(id.getStorageId()).getJField(storageId, type); // should always throw the appropriate exception assert false; } try { return type.cast(jfield); } catch (ClassCastException e) { throw new UnknownFieldException(storageId, jfield + "' is not a " + type.getSimpleName().replaceAll("^J(.*)Field$", "").toLowerCase() + " field"); } } private long getTypeFieldKey(int typeStorageId, int fieldStorageId) { return ((long)typeStorageId << 32) | (long)fieldStorageId & 0xffffffffL; } /** * 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.jclasses.size()); boolean invert = false; if (type == UntypedJObject.class) { type = null; invert = true; } this.getJClasses(type).stream() .map(jclass -> ObjId.getKeyRange(jclass.storageId)) .forEach(list::add); final KeyRanges keyRanges = new KeyRanges(list); return invert ? keyRanges.inverse() : keyRanges; } // Reference Paths /** * Parse a {@link ReferencePath} containing a target field. * *

* Equivalent to: {@link #parseReferencePath(Class, String, boolean) parseReferencePath}{@code (startType, path, true)}. * * @param startType starting Java type for the path * @param path dot-separated path of zero or more reference fields, followed by a target field * @return parsed reference path * @throws IllegalArgumentException if {@code path} is invalid * @throws IllegalArgumentException if {@code startType} or {@code path} is null * @see ReferencePath */ public ReferencePath parseReferencePath(Class startType, String path) { return this.parseReferencePath(startType, path, true); } /** * Parse a {@link ReferencePath}. * * @param startType starting Java type for the path * @param path dot-separated path of zero or more reference fields, followed by an optional target field * @param expectTargetField true if {@code path} contains a target field, false otherwise * @return parsed reference path * @throws IllegalArgumentException if {@code path} is invalid * @throws IllegalArgumentException if {@code startType} or {@code path} is null * @see ReferencePath */ public ReferencePath parseReferencePath(Class startType, String path, boolean expectTargetField) { return this.parseReferencePath(startType, path, expectTargetField, null); } ReferencePath parseReferencePath(Class startType, String path, boolean expectTargetField, Boolean lastIsSubField) { return this.referencePathCache.get(startType, path, expectTargetField, lastIsSubField); } // Validation /** * Configure a custom {@link ValidatorFactory} used to create {@link javax.validation.Validator}s * for validation within transactions. * * @param validatorFactory factory for validators * @throws IllegalArgumentException if {@code validatorFactory} is null */ public void setValidatorFactory(ValidatorFactory validatorFactory) { Preconditions.checkArgument(validatorFactory != null, "null validatorFactory"); this.validatorFactory = validatorFactory; } /** * Get the {@link ValidatorFactory}, if needed. * * @return {@link ValidatorFactory} for JSR 303 validation, or null if JSR 303 validation is not being used */ ValidatorFactory getValidatorFactory() { // Already created or configured? if (this.validatorFactory != null) return this.validatorFactory; // Are we doing any JSR 303 validation? if (this.elementRequiringJSR303Validation == null) return null; // Create it try { this.validatorFactory = Validation.buildDefaultValidatorFactory(); } catch (Exception e) { throw new PermazenException("JSR 303 validation constraint found on " + this.elementRequiringJSR303Validation + " but creation of default ValidatorFactory failed; is there a JSR 303 validation implementation on the classpath?", e); } // Done return this.validatorFactory; } // Misc utility /** * Utility method to get all of the objects directly referenced by a given object via any field. * *

* Note: the returned {@link Iterable} may contain duplicates; these can be eliminated using an * {@link io.permazen.core.util.ObjIdSet} if necessary. * * @param jobj starting object * @return all objects directly referenced by {@code jobj} * @throws IllegalArgumentException if {@code jobj} is null */ public Iterable getReferencedObjects(final JObject jobj) { // Sanity check Preconditions.checkArgument(jobj != null, "null jobj"); final ObjId id = jobj.getObjId(); // Visit fields final ArrayList> iterables = new ArrayList<>(); for (JField jfield : this.getJClass(id).getJFieldsByStorageId().values()) { jfield.visit(new JFieldSwitchAdapter() { @Override public Void caseJReferenceField(JReferenceField field) { final JObject ref = field.getValue(jobj); if (ref != null) iterables.add(Collections.singleton(ref)); return null; } @Override public Void caseJMapField(JMapField field) { if (field.getKeyField() instanceof JReferenceField) iterables.add(Iterables.filter(field.getValue(jobj).keySet(), JObject.class)); if (field.getValueField() instanceof JReferenceField) iterables.add(Iterables.filter(field.getValue(jobj).values(), JObject.class)); return null; } @Override protected Void caseJCollectionField(JCollectionField field) { if (field.getElementField() instanceof JReferenceField) iterables.add(Iterables.filter(field.getValue(jobj), JObject.class)); return null; } @Override protected Void caseJField(JField field) { return null; } }); } // Done return Iterables.concat(iterables); } // IndexQueryInfo Cache IndexQueryInfo getIndexQueryInfo(IndexQueryInfoKey key) { try { return this.indexQueryInfoCache.getUnchecked(key); } catch (UncheckedExecutionException e) { Throwables.throwIfUnchecked(e.getCause()); throw e; } } // Internal Stuff // Get class generator for "untyped" JObject's ClassGenerator getUntypedClassGenerator() { return this.untypedClassGenerator; } /** * Determine whether the specified field is a reference field. */ boolean isReferenceField(int storageId) { final IndexInfo info = this.indexInfoMap.get(storageId); return info instanceof SimpleFieldIndexInfo && ((SimpleFieldIndexInfo)info).getFieldType() instanceof ReferenceFieldType; } /** * Get the index info associated with the given storage ID. * * @param storageId index storage ID * @param type required type * @return {@link IndexInfo} instance * @throws IllegalArgumentException if {@code storageId} does not represent an index */ T getIndexInfo(int storageId, Class type) { Preconditions.checkArgument(type != null, "null type"); final IndexInfo indexInfo = this.indexInfoMap.get(storageId); if (indexInfo == null) { throw new IllegalArgumentException("no " + this.describe(type) + " with storage ID " + storageId + " exists in schema version " + this.actualVersion); } try { return type.cast(indexInfo); } catch (ClassCastException e) { throw new IllegalArgumentException("no " + this.describe(type) + " with storage ID " + storageId + " exists in schema version " + this.actualVersion + " (found field " + this.describe(type) + " instead)"); } } // Add new JClass, checking for storage ID conflicts private void addJClass(JClass jclass) { // Check for storage ID conflict final JClass other = this.jclasses.get(jclass.storageId); if (other != null) { throw new IllegalArgumentException("illegal duplicate use of storage ID " + jclass.storageId + " for both " + other + " and " + jclass); } this.jclasses.put(jclass.storageId, jclass); assert !this.jclassesByType.containsKey(jclass.type); // this should never conflict, no need to check this.jclassesByType.put(jclass.type, jclass); } // Add new index info, checking for storage ID conflicts private void addIndexInfo(JSchemaObject item, Map descriptionMap) { final IndexInfo info = item.toIndexInfo(); final int storageId = info.storageId; final IndexInfo existing = this.indexInfoMap.get(storageId); if (existing == null) { this.indexInfoMap.put(storageId, info); descriptionMap.put(storageId, item.description); } else if (!info.equals(existing)) { throw new IllegalArgumentException("incompatible duplicate use of storage ID " + item.storageId + " for " + descriptionMap.get(storageId) + " and " + item.description); } } private String describe(Class type) { return type.getSimpleName() .replaceAll("^(.*Index)Info$", "$1") .replaceAll("([a-z])([A-Z])", "$1 $2") .toLowerCase(); } // Loader private class Loader extends ClassLoader { // Set up class loader Loader() { super(Thread.currentThread().getContextClassLoader()); } // Find matching ClassGenerator, if any, otherwise defer to parent @Override protected Class findClass(String name) throws ClassNotFoundException { byte[] bytes = null; for (ClassGenerator generator : Permazen.this.classGenerators) { if (name.equals(generator.getClassName().replace('/', '.'))) { bytes = generator.generateBytecode(); break; } } return bytes != null ? this.defineClass(name, bytes, 0, bytes.length) : super.findClass(name); } } // CleanupCurrentCallback private static final class CleanupCurrentCallback extends Transaction.CallbackAdapter { private final JTransaction jtx; CleanupCurrentCallback(JTransaction jtx) { assert jtx != null; this.jtx = jtx; } @Override public void afterCompletion(boolean committed) { final JTransaction current; try { current = JTransaction.getCurrent(); } catch (IllegalStateException e) { return; } if (current == this.jtx) JTransaction.setCurrent(null); } @Override public int hashCode() { return this.jtx.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; final CleanupCurrentCallback that = (CleanupCurrentCallback)obj; return this.jtx.equals(that.jtx); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy