Package com.sleepycat.persist.impl

Source Code of com.sleepycat.persist.impl.PersistCatalog

/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002, 2011 Oracle and/or its affiliates.  All rights reserved.
*
*/

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.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.utilint.IdentityHashMap;
/* <!-- begin JE only --> */
import com.sleepycat.je.rep.ReplicaWriteException;
/* <!-- end JE only --> */
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<Format> 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.
     *
     * <p>This field, like formatMap, is volatile because it is reassigned
     * when dynamically adding new formats.  See {@link addNewFormat}.</p>
     */
    private volatile List<Format> formatList;

    /**
     * A map of the current/live formats in formatList, indexed by class name.
     *
     * <p>This field, like formatList, is volatile because it is reassigned
     * when dynamically adding new formats.  See {@link addNewFormat}.</p>
     */
    private volatile Map<String, Format> formatMap;

    /**
     * A map of the latest formats (includes deleted formats) in formatList,
     * indexed by class name.
     *
     * <p>This field, like formatMap, is volatile because it is reassigned
     * when dynamically adding new formats.  See {@link addNewFormat}.</p>
     */
    private volatile Map<String, Format> 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<String, String> 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<String, Format> formatMap = new HashMap<String, Format>();
                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<String, Format>(formatList.size());
            latestFormatMap = new HashMap<String, Format>(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<String> knownClasses =
                new ArrayList<String>(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<String, String>();
            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<String, Format> newFormats = new HashMap<String, Format>();
            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) {

                /* <!-- begin JE only --> */
                try {
                /* <!-- end JE only --> */

                    /*
                     * 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);
                /* <!-- begin JE only --> */
                } catch (ReplicaWriteException e) {
                    readOnly = true;
                }
                /* <!-- end JE only --> */
            }
            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<Format> 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<String> 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<Format, Set<Format>> getSubclassMap() {
        Map<Format, Set<Format>> subclassMap =
            new HashMap<Format, Set<Format>>();
        for (Format format : formatList) {
            if (format == null || Format.isPredefined(format)) {
                continue;
            }
            Format superFormat = format.getSuperFormat();
            if (superFormat != null) {
                Set<Format> subclass = subclassMap.get(superFormat);
                if (subclass == null) {
                    subclass = new HashSet<Format>();
                    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<String, Format> 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<String, Format> 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<String, Format> 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<Format> list,
                           Map<String, Format> 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<String> getModelClasses() {
        Set<String> classes = new HashSet<String>();
        for (Format format : formatMap.values()) {
            if (format.isModelClass()) {
                classes.add(format.getClassName());
            }
        }
        return Collections.unmodifiableSet(classes);
    }

    /**
     * Returns all formats as RawTypes.
     */
    public List<RawType> getAllRawTypes() {
        List<RawType> list = new ArrayList<RawType>();
        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.
     *
     * <p>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]</p>
     *
     * <p>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].</p>
     */
    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.
     *
     * <p>This method uses a copy-on-write technique to add new formats without
     * impacting other threads.</p>
     */
    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<Format> newFormatList = new ArrayList<Format>(formatList);
        Map<String, Format> newFormatMap =
            new HashMap<String, Format>(formatMap);
        Map<String, Format> newLatestFormatMap =
            new HashMap<String, Format>(latestFormatMap);

        /* Add the new format and all related new formats. */
        Map<String, Format> newFormats = new HashMap<String, Format>();
        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);
            /* <!-- begin JE only --> */
            } catch (ReplicaWriteException e) {
                readOnly = true;
            /* <!-- end JE only --> */
            } 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));
    }
}
TOP

Related Classes of com.sleepycat.persist.impl.PersistCatalog

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.