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

com.sleepycat.persist.impl.PersistCatalog Maven / Gradle / Ivy

Go to download

Berkeley DB Java Edition is a open source, transactional storage solution for Java applications. The Direct Persistence Layer (DPL) API is faster and easier to develop, deploy, and manage than serialized object files or ORM-based Java persistence solutions. The Collections API enhances the standard java.util.collections classes allowing them to be persisted to a local file system and accessed concurrently while protected by ACID transactions. Data is stored by serializing objects and managing class and instance data separately so as not to waste space. Berkeley DB Java Edition is the reliable drop-in solution for complex, fast, and scalable storage. Source for this release is in 'je-4.0.92-sources.jar', the Javadoc is located at 'http://download.oracle.com/berkeley-db/docs/je/4.0.92/'.

There is a newer version: 5.0.73
Show newest version
/*-
 * Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle Berkeley
 * DB Java Edition made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle Berkeley DB Java Edition for a copy of the
 * license and additional information.
 */

package com.sleepycat.persist.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.compat.DbCompat;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.LockConflictException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
/*  */
import com.sleepycat.je.rep.ReplicaWriteException;
/*  */
import com.sleepycat.persist.DatabaseNamer;
import com.sleepycat.persist.StoreExistsException;
import com.sleepycat.persist.StoreNotFoundException;
import com.sleepycat.persist.evolve.Converter;
import com.sleepycat.persist.evolve.IncompatibleClassException;
import com.sleepycat.persist.evolve.Mutations;
import com.sleepycat.persist.evolve.Renamer;
import com.sleepycat.persist.model.AnnotationModel;
import com.sleepycat.persist.model.ClassMetadata;
import com.sleepycat.persist.model.EntityMetadata;
import com.sleepycat.persist.model.EntityModel;
import com.sleepycat.persist.model.ModelInternal;
import com.sleepycat.persist.raw.RawObject;
import com.sleepycat.persist.raw.RawType;
import com.sleepycat.util.RuntimeExceptionWrapper;

/**
 * The catalog of class formats for a store, along with its associated model
 * and mutations.
 *
 * @author Mark Hayes
 */
public class PersistCatalog implements Catalog {
    
    private static final int MAX_TXN_RETRIES = 10;

    /**
     * Key to Data record in the catalog database.  In the JE 3.0.12 beta
     * version the formatList record is stored under this key and is converted
     * to a Data object when it is read.
     */
    private static final byte[] DATA_KEY = getIntBytes(-1);

    /**
     * Key to a JE 3.0.12 beta version mutations record in the catalog
     * database.  This record is no longer used because mutations are stored in
     * the Data record and is deleted when the beta version is detected.
     */
    private static final byte[] BETA_MUTATIONS_KEY = getIntBytes(-2);

    private static byte[] getIntBytes(int val) {
        DatabaseEntry entry = new DatabaseEntry();
        IntegerBinding.intToEntry(val, entry);
        assert entry.getSize() == 4 && entry.getData().length == 4;
        return entry.getData();
    }

    /**
     * Used by unit tests.
     */
    public static boolean expectNoClassChanges;
    public static boolean unevolvedFormatsEncountered;

    /**
     * The object stored under DATA_KEY in the catalog database.
     */
    private static class Data implements Serializable {

        static final long serialVersionUID = 7515058069137413261L;

        List formatList;
        Mutations mutations;
        int version;
    }

    /**
     * A list of all formats indexed by formatId.  Element zero is unused and
     * null, since IDs start at one; this avoids adjusting the ID to index the
     * list.  Some elements are null to account for predefined IDs that are not
     * used.
     *
     * 

This field, like formatMap, is volatile because it is reassigned * when dynamically adding new formats. See {@link #addNewFormat}.

*/ private volatile List formatList; /** * A map of the current/live formats in formatList, indexed by class name. * *

This field, like formatList, is volatile because it is reassigned * when dynamically adding new formats. See {@link #addNewFormat}.

*/ private volatile Map formatMap; /** * A map of the latest formats (includes deleted formats) in formatList, * indexed by class name. * *

This field, like formatMap, is volatile because it is reassigned * when dynamically adding new formats. See {@link #addNewFormat}.

*/ private volatile Map latestFormatMap; /** * A temporary map of proxied class name to proxy class name. Used during * catalog creation, and then set to null. This map is used to force proxy * formats to be created prior to proxied formats. [#14665] */ private Map proxyClassMap; private final Environment env; private final boolean rawAccess; private EntityModel model; private StoredModel storedModel; private Mutations mutations; private final Database db; private int openCount; private boolean readOnly; private final boolean transactional; /** * If a Replica is upgraded, local in-memory evolution may take place prior * to the Master being upgraded. In that case, the size of the formatList * will be greater than nStoredFormats. In this case, the readOnly * state field will be set to true. We must be sure not to write the * metadata in this state. [#16655] */ private volatile int nStoredFormats; /** * The Store is normally present but may be null in unit tests (for * example, BindingTest). */ private final Store store; /** * The Evolver and catalog Data are non-null during catalog initialization, * and null otherwise. */ private Evolver initEvolver; private Data initData; /** * Creates a new catalog, opening the database and reading it from a given * catalog database if it already exists. All predefined formats and * formats for the given model are added. For modified classes, old * formats are defined based on the rules for compatible class changes and * the given mutations. If any format is changed or added, and the * database is not read-only, write the initialized catalog to the * database. */ public PersistCatalog(final Environment env, final String storePrefix, final String dbName, final DatabaseConfig dbConfig, final EntityModel modelParam, final Mutations mutationsParam, final boolean rawAccess, final Store store) throws StoreExistsException, StoreNotFoundException, IncompatibleClassException, DatabaseException { this.env = env; this.rawAccess = rawAccess; this.store = store; this.transactional = dbConfig.getTransactional(); /* store may be null for testing. */ String[] fileAndDbNames = (store != null) ? store.parseDbName(dbName) : Store.parseDbName(dbName, DatabaseNamer.DEFAULT); /* * Use a null (auto-commit) transaction for opening the database, so * that the database is opened even if a ReplicaWriteException occurs * when attempting to evolve the metadata. We will close the database * if another exception occurs in the finally statement below. */ db = DbCompat.openDatabase(env, null /*txn*/, fileAndDbNames[0], fileAndDbNames[1], dbConfig); if (db == null) { String dbNameMsg = store.getDbNameMessage(fileAndDbNames); if (dbConfig.getExclusiveCreate()) { throw new StoreExistsException ("Catalog DB already exists and ExclusiveCreate=true, " + dbNameMsg); } else { assert !dbConfig.getAllowCreate(); throw new StoreNotFoundException ("Catalog DB does not exist and AllowCreate=false, " + dbNameMsg); } } openCount = 1; boolean success = false; try { initAndRetry(storePrefix, modelParam, mutationsParam); success = true; } finally { if (!success) { close(); } } } /** * Creates a new catalog when a Replica refresh occurs. Uses some * information from the old catalog directly in the new catalog, but all * formats are created from scratch and class evolution is attempted. */ PersistCatalog(final PersistCatalog oldCatalog, final String storePrefix) throws DatabaseException { db = oldCatalog.db; store = oldCatalog.store; env = oldCatalog.env; rawAccess = oldCatalog.rawAccess; openCount = oldCatalog.openCount; transactional = oldCatalog.transactional; initAndRetry(storePrefix, oldCatalog.model, oldCatalog.mutations); } private void initAndRetry(final String storePrefix, final EntityModel modelParam, final Mutations mutationsParam) throws DatabaseException { for (int i = 0;; i += 1) { Transaction txn = null; if (transactional && DbCompat.getThreadTransaction(env) == null) { txn = env.beginTransaction(null, store.getAutoCommitTxnConfig()); } boolean success = false; try { init(txn, storePrefix, modelParam, mutationsParam); success = true; return; } catch (LockConflictException e) { /* * It is very unlikely that two threads opening the same * EntityStore will cause a lock conflict. However, because we * read-modify-update the catalog record, * LockPreemptedException must be handled in a replicated JE * environment. Since LockPreemptedException is a * LockConfictException, it is simplest to retry when any * LockConfictException occurs. */ if (i >= MAX_TXN_RETRIES) { throw e; } continue; } finally { /* * If the catalog is read-only we abort rather than commit, * because a ReplicaWriteException may have occurred. * ReplicaWriteException invalidates the transaction, and there * are no writes to commit anyway. [#16655] */ if (txn != null) { if (success && !isReadOnly()) { txn.commit(); } else { txn.abort(); } } } } } private void init(final Transaction txn, final String storePrefix, final EntityModel modelParam, final Mutations mutationsParam) throws DatabaseException { try { initData = readData(txn); mutations = initData.mutations; if (mutations == null) { mutations = new Mutations(); } /* * When the beta version is detected, force a re-write of the * catalog and disallow class changes. This brings the catalog up * to date so that evolution can proceed correctly from then on. */ boolean betaVersion = (initData.version == BETA_VERSION); boolean needWrite = betaVersion; boolean disallowClassChanges = betaVersion; /* * Store the given mutations if they are different from the stored * mutations, and force evolution to apply the new mutations. */ boolean forceEvolution = false; if (mutationsParam != null && !mutations.equals(mutationsParam)) { mutations = mutationsParam; needWrite = true; forceEvolution = true; } final ClassLoader envClassLoader = DbCompat.getClassLoader(env); /* Get the existing format list, or copy it from SimpleCatalog. */ formatList = initData.formatList; if (formatList == null) { formatList = SimpleCatalog.getAllSimpleFormats(envClassLoader); /* * Special cases: Object and Number are predefined but are not * simple types. */ Format format = new NonPersistentFormat(this, Object.class); format.setId(Format.ID_OBJECT); formatList.set(Format.ID_OBJECT, format); format = new NonPersistentFormat(this, Number.class); format.setId(Format.ID_NUMBER); formatList.set(Format.ID_NUMBER, format); } else { /* Pick up any new predefined simple types. */ if (SimpleCatalog.addMissingSimpleFormats(envClassLoader, formatList)) { needWrite = true; } nStoredFormats = formatList.size(); } /* Initialize transient catalog field before further use. */ for (Format format : formatList) { if (format != null) { format.initCatalog(this); } } /* Special handling for JE 3.0.12 beta formats. */ if (betaVersion) { Map formatMap = new HashMap(); for (Format format : formatList) { if (format != null) { formatMap.put(format.getClassName(), format); } } for (Format format : formatList) { if (format != null) { format.migrateFromBeta(formatMap); } } } /* * If we should not use the current model, initialize the stored * model and return. */ formatMap = new HashMap(formatList.size()); latestFormatMap = new HashMap(formatList.size()); if (rawAccess) { for (Format format : formatList) { if (format != null) { String name = format.getClassName(); if (format.isCurrentVersion()) { formatMap.put(name, format); } if (format == format.getLatestVersion()) { latestFormatMap.put(name, format); } } } if (modelParam != null) { model = modelParam; storedModel = (StoredModel) modelParam; } else { storedModel = new StoredModel(this); model = storedModel; } ModelInternal.setClassLoader(model, envClassLoader); for (Format format : formatList) { if (format != null) { format.initializeIfNeeded(this, model); } } initModelAndMutations(); return; } /* * We are opening a store that uses the current model. Default to * the AnnotationModel if no model is specified. */ if (modelParam != null) { model = modelParam; } else { model = new AnnotationModel(); } ModelInternal.setClassLoader(model, envClassLoader); storedModel = null; /* * Add all predefined (simple) formats to the format map. The * current version of other formats will be added below. */ for (int i = 0; i <= Format.ID_PREDEFINED; i += 1) { Format simpleFormat = formatList.get(i); if (simpleFormat != null) { formatMap.put(simpleFormat.getClassName(), simpleFormat); } } /* * Known classes are those explicitly registered by the user via * the model, plus the predefined proxy classes. */ List knownClasses = new ArrayList(model.getKnownClasses()); /* Also adds the special classes, i.e., enum or array. [#19377] */ knownClasses.addAll(model.getKnownSpecialClasses()); addPredefinedProxies(knownClasses); /* * Create a temporary map of proxied class name to proxy class * name, using all known formats and classes. This map is used to * force proxy formats to be created prior to proxied formats. * [#14665] */ proxyClassMap = new HashMap(); for (Format oldFormat : formatList) { if (oldFormat == null || Format.isPredefined(oldFormat)) { continue; } String oldName = oldFormat.getClassName(); Renamer renamer = mutations.getRenamer (oldName, oldFormat.getVersion(), null); String newName = (renamer != null) ? renamer.getNewName() : oldName; addProxiedClass(newName, false /*isKnownClass*/); } for (String className : knownClasses) { addProxiedClass(className, true /*isKnownClass*/); } /* * Add known formats from the model and the predefined proxies. * In general, classes will not be present in an AnnotationModel * until an instance is stored, in which case an old format exists. * However, registered proxy classes are an exception and must be * added in advance. And the user may choose to register new * classes in advance. The more formats we define in advance, the * less times we have to write to the catalog database. */ Map newFormats = new HashMap(); for (String className : knownClasses) { createFormat(className, newFormats); } /* * Perform class evolution for all old formats, and throw an * exception that contains the messages for all of the errors in * mutations or in the definition of new classes. */ initEvolver = new Evolver (this, storePrefix, mutations, newFormats, forceEvolution, disallowClassChanges); for (Format oldFormat : formatList) { if (oldFormat == null || Format.isPredefined(oldFormat)) { continue; } if (oldFormat.isEntity()) { initEvolver.evolveFormat(oldFormat); } else { initEvolver.addNonEntityFormat(oldFormat); } } initEvolver.finishEvolution(); String errors = initEvolver.getErrors(); if (errors != null) { throw new IncompatibleClassException(errors); } /* * Add the new formats remaining. New formats that are equal to * old formats were removed from the newFormats map above. */ for (Format newFormat : newFormats.values()) { addFormat(newFormat); } /* Initialize all formats. */ for (Format format : formatList) { if (format != null) { format.initializeIfNeeded(this, model); if (format == format.getLatestVersion()) { latestFormatMap.put(format.getClassName(), format); } } } final boolean formatsChanged = newFormats.size() > 0 || initEvolver.areFormatsChanged(); needWrite |= formatsChanged; /* For unit testing. */ if (expectNoClassChanges && formatsChanged) { throw new IllegalStateException ("Unexpected changes " + " newFormats.size=" + newFormats.size() + " areFormatsChanged=" + initEvolver.areFormatsChanged()); } readOnly = db.getConfig().getReadOnly(); /* Write the catalog if anything changed. */ if (needWrite && !readOnly) { /* */ try { /* */ /* * Only rename/remove databases if we are going to update * the catalog to reflect those class changes. */ initEvolver.renameAndRemoveDatabases(store, txn); /* * Note that we use the Data object that was read above, * and the beta version determines whether to delete the * old mutations record. */ initData.formatList = formatList; initData.mutations = mutations; writeData(txn, initData); /* */ } catch (ReplicaWriteException e) { readOnly = true; } /* */ } initModelAndMutations(); } finally { /* * Fields needed only for the duration of this ctor and which * should be null afterwards. */ proxyClassMap = null; initData = null; initEvolver = null; } } private void initModelAndMutations() { /* * Give the model a reference to the catalog to fully initialize * the model. Only then may we initialize the Converter mutations, * which themselves may call model methods and expect the model to * be fully initialized. */ ModelInternal.setCatalog(model, this); for (Converter converter : mutations.getConverters()) { converter.getConversion().initialize(model); } } public void getEntityFormats(Collection entityFormats) { for (Format format : formatMap.values()) { if (format.isEntity()) { entityFormats.add(format); } } } private void addProxiedClass(String className, boolean isKnownClass) { ClassMetadata metadata = model.getClassMetadata(className); if (metadata != null) { String proxiedClassName = metadata.getProxiedClassName(); if (proxiedClassName != null) { /* * If the class is a registered known class, need to check if * registering proxy class is allowed or not. Currently, only * SimpleType is not allowed to register a proxy class. */ if (isKnownClass) { try { Class type = resolveClass(proxiedClassName); /* * Check if the proxied class is allowed to register a * proxy class. If not, IllegalArgumentException will * be thrown. */ if(!SimpleCatalog.allowRegisterProxy(type)) { throw new IllegalArgumentException ("Registering proxy is not allowed for " + proxiedClassName + ", which is a built-in simple type."); } } catch (ClassNotFoundException e) { throw DbCompat.unexpectedState ("Class does not exist: " + proxiedClassName); } } proxyClassMap.put(proxiedClassName, className); } } } private void addPredefinedProxies(List knownClasses) { knownClasses.add(CollectionProxy.ArrayListProxy.class.getName()); knownClasses.add(CollectionProxy.LinkedListProxy.class.getName()); knownClasses.add(CollectionProxy.HashSetProxy.class.getName()); knownClasses.add(CollectionProxy.TreeSetProxy.class.getName()); knownClasses.add(MapProxy.HashMapProxy.class.getName()); knownClasses.add(MapProxy.TreeMapProxy.class.getName()); knownClasses.add(MapProxy.LinkedHashMapProxy.class.getName()); } /** * Returns a map from format to a set of its superclass formats. The * format for simple types, enums and class Object are not included. Only * complex types have superclass formats as defined by * Format.getSuperFormat. */ Map> getSubclassMap() { Map> subclassMap = new HashMap>(); for (Format format : formatList) { if (format == null || Format.isPredefined(format)) { continue; } Format superFormat = format.getSuperFormat(); if (superFormat != null) { Set subclass = subclassMap.get(superFormat); if (subclass == null) { subclass = new HashSet(); subclassMap.put(superFormat, subclass); } subclass.add(format); } } return subclassMap; } /** * Returns the model parameter, default model or stored model. */ public EntityModel getResolvedModel() { return model; } /** * Increments the reference count for a catalog that is already open. */ public void openExisting() { openCount += 1; } /** * Returns true if the user opened the store read-only, or we're running in * Replica upgrade mode. */ public boolean isReadOnly() { return readOnly; } /** * Decrements the reference count and closes the catalog DB when it reaches * zero. Returns true if the database was closed or false if the reference * count is still non-zero and the database was left open. */ public boolean close() throws DatabaseException { if (openCount == 0) { throw DbCompat.unexpectedState("Catalog is not open"); } else { openCount -= 1; if (openCount == 0) { db.close(); return true; } else { return false; } } } /** * Returns the current merged mutations. */ public Mutations getMutations() { return mutations; } /** * Convenience method that gets the class for the given class name and * calls createFormat with the class object. */ public Format createFormat(String clsName, Map newFormats) { Class type; try { type = resolveClass(clsName); } catch (ClassNotFoundException e) { throw DbCompat.unexpectedState ("Class does not exist: " + clsName); } return createFormat(type, newFormats); } /** * If the given class format is not already present in the given map and * a format for this class name does not already exist, creates an * uninitialized format, adds it to the map, and also collects related * formats in the map. */ public Format createFormat(Class type, Map newFormats) { /* Return a new or existing format for this class. */ String className = type.getName(); Format format = getFormatFromMap(type, newFormats); if (format != null) { return format; } format = getFormatFromMap(type, formatMap); if (format != null) { return format; } /* Simple types are predefined. */ assert !SimpleCatalog.isSimpleType(type) : className; /* * Although metadata is only needed for a complex type, call * getClassMetadata for all types to support checks for illegal * metadata on other types. */ ClassMetadata metadata = model.getClassMetadata(className); /* Create format of the appropriate type. */ String proxyClassName = null; if (proxyClassMap != null) { proxyClassName = proxyClassMap.get(className); } if (proxyClassName != null) { format = new ProxiedFormat(this, type, proxyClassName); } else if (type.isArray()) { format = type.getComponentType().isPrimitive() ? (new PrimitiveArrayFormat(this, type)) : (new ObjectArrayFormat(this, type)); } else if (type.isEnum()) { format = new EnumFormat(this, type); } else if (type.getEnclosingClass() != null && type.getEnclosingClass().isEnum()) { /* * If the type is an anonymous class of an enum class, the format * which represents the enum class will be created. [#18357] */ format = new EnumFormat(this, type.getEnclosingClass()); } else if (type == Object.class || type.isInterface()) { format = new NonPersistentFormat(this, type); } else { if (metadata == null) { throw new IllegalArgumentException ("Class could not be loaded or is not persistent: " + className); } if (metadata.getCompositeKeyFields() != null && (metadata.getPrimaryKey() != null || metadata.getSecondaryKeys() != null)) { throw new IllegalArgumentException ("A composite key class may not have primary or" + " secondary key fields: " + type.getName()); } /* * Check for inner class before default constructor, to give a * specific error message for each. */ if (type.getEnclosingClass() != null && !Modifier.isStatic(type.getModifiers())) { throw new IllegalArgumentException ("Inner classes not allowed: " + type.getName()); } try { type.getDeclaredConstructor(); } catch (NoSuchMethodException e) { throw new IllegalArgumentException ("No default constructor: " + type.getName(), e); } if (metadata.getCompositeKeyFields() != null) { format = new CompositeKeyFormat (this, type, metadata, metadata.getCompositeKeyFields()); } else { EntityMetadata entityMetadata = model.getEntityMetadata(className); format = new ComplexFormat(this, type, metadata, entityMetadata); } } /* Collect new format along with any related new formats. */ newFormats.put(className, format); format.collectRelatedFormats(this, newFormats); return format; } private Format getFormatFromMap(Class type, Map formats) { Format format = formats.get(type.getName()); if (format != null) { return format; } else if (type.getEnclosingClass() != null && type.getEnclosingClass().isEnum()) { /* * If the type is an anonymous class of this enum class, the format * which represents the enum class will be returned. [#18357] */ format = formats.get(type.getEnclosingClass().getName()); if (format != null) { return format; } } return null; } /** * Adds a format and makes it the current format for the class. */ private void addFormat(Format format) { addFormat(format, formatList, formatMap); } /** * Adds a format to the given the format collections, for use when * dynamically adding formats. */ private void addFormat(Format format, List list, Map map) { format.setId(list.size()); list.add(format); map.put(format.getClassName(), format); } /** * Installs an existing format when no evolution is needed, i.e, when the * new and old formats are identical. */ void useExistingFormat(Format oldFormat) { assert oldFormat.isCurrentVersion(); formatMap.put(oldFormat.getClassName(), oldFormat); } /** * Returns a set of all persistent (non-simple type) class names. */ Set getModelClasses() { Set classes = new HashSet(); for (Format format : formatMap.values()) { if (format.isModelClass()) { classes.add(format.getClassName()); } } return Collections.unmodifiableSet(classes); } /** * Returns all formats as RawTypes. */ public List getAllRawTypes() { List list = new ArrayList(); for (RawType type : formatList) { if (type != null) { list.add(type); } } return Collections.unmodifiableList(list); } /** * When a format is intialized, this method is called to get the version * of the serialized object to be initialized. See Catalog. */ public int getInitVersion(Format format, boolean forReader) { if (initData == null || initData.formatList == null || format.getId() >= initData.formatList.size()) { /* * For new formats, use the current version. If initData is null, * the Catalog ctor is finished and the format must be new. If the * ctor is in progress, the format is new if its ID is greater than * the ID of all pre-existing formats. */ return Catalog.CURRENT_VERSION; } else { /* * Get the version of a pre-existing format during execution of the * Catalog ctor. The initData field is non-null, but initEvolver * may be null if the catalog is opened in raw mode. */ assert initData != null; if (forReader) { /* * Get the version of the evolution reader for a pre-existing * format. Use the current version if the format changed * during class evolution, otherwise use the stored version. */ return (initEvolver != null && initEvolver.isFormatChanged(format)) ? Catalog.CURRENT_VERSION : initData.version; } else { /* Always used the stored version for a pre-existing format. */ return initData.version; } } } public Format getFormat(final int formatId, final boolean expectStored) throws RefreshException { if (formatId < 0) { throw DbCompat.unexpectedState ("Format ID " + formatId + " is negative," + " may indicate data corruption."); } /** * If we're attempting to read a record containing a format ID that is * greater than the maximum known stored format, then we refresh the * formats from disk, expecting that the stored formats have been * updated by the Master node. Note that format IDs greater than * nStoredFormats may exist in the formatList, if evolution took place * on this Replica in a read-only mode. Such formats are never written * (Replicas do not write) and cannot be used for reading an existing * record. [#16655] * * Do not perform this check if we did not get the format ID from a * stored record (expectStored is false). For example, this would cause * an erroneous RefreshException when this method is called during a * convertRawObject operation, which calls this method to get a fresh * copy of a format that may not be stored. [#18690] */ if (expectStored && formatId >= nStoredFormats) { assert store != null; throw new RefreshException(store, this, formatId); } Format format = formatList.get(formatId); if (format == null) { throw DbCompat.unexpectedState ("Format ID " + formatId + " has null format," + " may indicate data corruption."); } /* * Currently we can't throw DeletedClassException because we should not * do this if we're being called during a Conversion, and we don't have * that state information available. */ /* if (format.isDeleted()) { throw new DeletedClassException ("Class " + format.getClassName() + " was deleted with a Deleter muation, format ID " + formatId + '.'); } */ return format; } /** * Get a format for a given class, creating it if it does not exist. * *

This method is called for top level entity instances by * PersistEntityBinding. When a new entity subclass format is added we * call Store.checkEntitySubclassSecondaries to ensure that all secondary * databases have been opened, before storing the entity. We do this here * while not holding a synchronization mutex, not in addNewFormat, to avoid * deadlocks. checkEntitySubclassSecondaries synchronizes on the Store. * [#16399]

* *

Historical note: At one time we opened / created the secondary * databases rather than requiring the user to open them, see [#15247]. * Later we found this to be problematic since a user txn may have locked * primary records, see [#16399].

*/ public Format getFormat(Class cls, boolean checkEntitySubclassIndexes) throws RefreshException { Format format = formatMap.get(cls.getName()); if (format == null) { if (model != null) { format = addNewFormat(cls); /* Detect and handle new entity subclass. [#15247] */ if (checkEntitySubclassIndexes && store != null) { Format entityFormat = format.getEntityFormat(); if (entityFormat != null && entityFormat != format) { try { store.checkEntitySubclassSecondaries (entityFormat.getEntityMetadata(), cls.getName()); } catch (DatabaseException e) { throw RuntimeExceptionWrapper.wrapIfNeeded(e); } } } } if (format == null) { throw new IllegalArgumentException ("Class is not persistent: " + cls.getName()); } } return format; } public Format getFormat(String className) { return formatMap.get(className); } public Format getLatestVersion(String className) { return latestFormatMap.get(className); } /** * Returns the name of an entity class to be used to form the database * name. Normally this is the same as the class name, but in replica * upgrade mode it may be an earlier version of a renamed class. Returns * null if there is no stored version of the class. [#16655] */ public String getDatabaseClassName(final String className) { final Format format = getStoredFormat(className); if (format == null) { return null; } return format.getClassName(); } /** * Similar to getDatabaseClassName but instead handles an earlier version * of a renamed key. [#16655] */ public String getDatabaseKeyName(final String className, final String keyName) { final Format format = getStoredFormat(className); if (format == null) { return null; } return format.getOldKeyName(keyName); } private Format getStoredFormat(final String className) { Format format = getFormat(className); while (format != null && format.getId() >= nStoredFormats) { format = format.getPreviousVersion(); } return format; } /** * Metadata needs refreshing when a Replica with stale metadata is elected * master, and then a user write operation is attempted. [#16655] */ void checkWriteInReplicaUpgradeMode() throws RefreshException { if (nStoredFormats < formatList.size()) { throw new RefreshException(store, this, -1 /*formatId*/); } } /** * For unit testing. */ boolean isReplicaUpgradeMode() { return nStoredFormats < formatList.size(); } /** * Adds a format for a new class. Returns the format added for the given * class, or throws an exception if the given class is not persistent. * *

This method uses a copy-on-write technique to add new formats without * impacting other threads.

*/ private synchronized Format addNewFormat(Class cls) throws RefreshException { /* * After synchronizing, check whether another thread has added the * format needed. Note that this is not the double-check technique * because the formatMap field is volatile and is not itself checked * for null. (The double-check technique is known to be flawed in * Java.) */ Format format = getFormatFromMap(cls, formatMap); if (format != null) { return format; } /* Copy the read-only format collections. */ List newFormatList = new ArrayList(formatList); Map newFormatMap = new HashMap(formatMap); Map newLatestFormatMap = new HashMap(latestFormatMap); /* Add the new format and all related new formats. */ Map newFormats = new HashMap(); format = createFormat(cls, newFormats); for (Format newFormat : newFormats.values()) { addFormat(newFormat, newFormatList, newFormatMap); } /* * Initialize new formats using a read-only catalog because we can't * update this catalog until after we store it (below). */ Catalog newFormatCatalog = new ReadOnlyCatalog (ModelInternal.getClassLoader(model), newFormatList, newFormatMap); for (Format newFormat : newFormats.values()) { newFormat.initializeIfNeeded(newFormatCatalog, model); newLatestFormatMap.put(newFormat.getClassName(), newFormat); } /* * Write the updated catalog using auto-commit, then assign the new * collections. The database write must occur before the collections * are used, since a format must be persistent before it can be * referenced by a data record. * * In readOnly mode, which includes Replica upgrade mode, we should not * attempt to write since we could be elected Master and write stale * metadata. If ReplicaWriteException occurs then we transition to * Replica upgrade mode in the same manner as in the init() method. * This can happen when no schema change is made except for one or more * new entity classes. The new entity class will not be detected by * evolution (during init()) but will be detected here if the user * calls getPrimaryIndex. [#16655] */ if (!readOnly) { try { Data newData = new Data(); newData.formatList = newFormatList; newData.mutations = mutations; writeDataCheckStale(newData); /* */ } catch (ReplicaWriteException e) { readOnly = true; /* */ } catch (DatabaseException e) { throw RuntimeExceptionWrapper.wrapIfNeeded(e); } } formatList = newFormatList; formatMap = newFormatMap; latestFormatMap = newLatestFormatMap; return format; } /** * Used to write the catalog when a format has been changed, for example, * when Store.evolve has updated a Format's EvolveNeeded property. Uses * auto-commit. */ public synchronized void flush(Transaction txn) throws DatabaseException { Data newData = new Data(); newData.formatList = formatList; newData.mutations = mutations; writeData(txn, newData); } /** * Returns the number of stored formats. */ int getNFormats() { return nStoredFormats; } /** * Reads catalog Data, converting old versions as necessary. An empty * Data object is returned if no catalog data currently exists. Null is * never returned. */ private Data readData(Transaction txn) throws DatabaseException { Data oldData; DatabaseEntry key = new DatabaseEntry(DATA_KEY); DatabaseEntry data = new DatabaseEntry(); OperationStatus status = db.get(txn, key, data, null); if (status == OperationStatus.SUCCESS) { ByteArrayInputStream bais = new ByteArrayInputStream (data.getData(), data.getOffset(), data.getSize()); try { ObjectInputStream ois = new ObjectInputStream(bais); Object object = ois.readObject(); assert ois.available() == 0; if (object instanceof Data) { oldData = (Data) object; } else { if (!(object instanceof List)) { throw DbCompat.unexpectedState (object.getClass().getName()); } oldData = new Data(); oldData.formatList = (List) object; oldData.version = BETA_VERSION; } return oldData; } catch (ClassNotFoundException e) { throw DbCompat.unexpectedException(e); } catch (IOException e) { throw DbCompat.unexpectedException(e); } } else { oldData = new Data(); oldData.version = Catalog.CURRENT_VERSION; } return oldData; } /** * Metadata needs refreshing when a Replica with stale metadata is elected * master, and then a user write operation is attempted that also requires * a metadata update. [#16655] */ boolean isMetadataStale(Transaction txn) throws DatabaseException { Data oldData = readData(txn); return (oldData.formatList != null && oldData.formatList.size() > nStoredFormats); } /** * Writes catalog Data after checking for stale metadata. */ private void writeDataCheckStale(Data newData) throws DatabaseException, RefreshException { for (int i = 0;; i += 1) { Transaction txn = null; if (transactional && DbCompat.getThreadTransaction(env) == null) { txn = env.beginTransaction(null, store.getAutoCommitTxnConfig()); } boolean success = false; try { if (isMetadataStale(txn)) { throw new RefreshException(store, this, -1 /*formatId*/); } writeData(txn, newData); success = true; return; } catch (LockConflictException e) { /* * A lock conflict should not occur because writes to the * catalog DB are in synchronized methods. However, because we * read-modify-update the catalog record, * LockPreemptedException must be handled in a replicated JE * environment. Since LockPreemptedException is a * LockConfictException, it is simplest to retry when any * LockConfictException occurs. */ if (i >= MAX_TXN_RETRIES) { throw e; } continue; } finally { /* * If the catalog is read-only we abort rather than commit, * because a ReplicaWriteException may have occurred. * ReplicaWriteException invalidates the transaction, and there * are no writes to commit anyway. [#16655] */ if (txn != null) { if (success && !isReadOnly()) { txn.commit(); } else { txn.abort(); } } } } } /** * Writes catalog Data. Does not check for stale metadata. */ private void writeData(Transaction txn, Data newData) throws DatabaseException { /* Catalog data is written in the current version. */ boolean wasBetaVersion = (newData.version == BETA_VERSION); newData.version = CURRENT_VERSION; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(newData); } catch (IOException e) { throw DbCompat.unexpectedException(e); } DatabaseEntry key = new DatabaseEntry(DATA_KEY); DatabaseEntry data = new DatabaseEntry(baos.toByteArray()); db.put(txn, key, data); /* * Delete the unused beta mutations record if we read the beta version * record earlier. */ if (wasBetaVersion) { key.setData(BETA_MUTATIONS_KEY); db.delete(txn, key); } nStoredFormats = newData.formatList.size(); } public boolean isRawAccess() { return rawAccess; } public Object convertRawObject(RawObject o, IdentityHashMap converted) throws RefreshException { Format format = (Format) o.getType(); if (this == format.getCatalog()) { /* Ensure a fresh format is used, in case of Replica refresh. */ format = getFormat(format.getId(), false /*expectStored*/); } else { /* * Use the corresponding format in this catalog when the external * raw object was created using a different catalog. Create the * format if it does not already exist, for example, when this * store is empty. [#16253]. */ String clsName = format.getClassName(); Class cls; try { cls = resolveClass(clsName); format = getFormat(cls, true /*checkEntitySubclassIndexes*/); } catch (ClassNotFoundException e) { format = null; } if (format == null) { throw new IllegalArgumentException ("External raw type not found: " + clsName); } } Format proxiedFormat = format.getProxiedFormat(); if (proxiedFormat != null) { format = proxiedFormat; } if (converted == null) { converted = new IdentityHashMap(); } return format.convertRawObject(this, false, o, converted); } public Class resolveClass(String clsName) throws ClassNotFoundException { return SimpleCatalog.resolveClass (clsName, ModelInternal.getClassLoader(model)); } public Class resolveKeyClass(String clsName) { return SimpleCatalog.resolveKeyClass (clsName, ModelInternal.getClassLoader(model)); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy