1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2002,2008 Oracle.  All rights reserved.
5 *
6 * $Id: PersistCatalog.java,v 1.2 2008/02/08 20:12:37 mark Exp $
7 */
8
9package com.sleepycat.persist.impl;
10
11import java.io.ByteArrayInputStream;
12import java.io.ByteArrayOutputStream;
13import java.io.FileNotFoundException;
14import java.io.IOException;
15import java.io.ObjectInputStream;
16import java.io.ObjectOutputStream;
17import java.io.Serializable;
18import java.util.ArrayList;
19import java.util.Collection;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.IdentityHashMap;
23import java.util.List;
24import java.util.Map;
25import java.util.NoSuchElementException;
26import java.util.Set;
27
28import com.sleepycat.bind.tuple.IntegerBinding;
29import com.sleepycat.compat.DbCompat;
30import com.sleepycat.db.Database;
31import com.sleepycat.db.DatabaseConfig;
32import com.sleepycat.db.DatabaseEntry;
33import com.sleepycat.db.DatabaseException;
34import com.sleepycat.db.Environment;
35import com.sleepycat.db.OperationStatus;
36import com.sleepycat.db.Transaction;
37import com.sleepycat.persist.DatabaseNamer;
38import com.sleepycat.persist.evolve.DeletedClassException;
39import com.sleepycat.persist.evolve.IncompatibleClassException;
40import com.sleepycat.persist.evolve.Mutations;
41import com.sleepycat.persist.evolve.Renamer;
42import com.sleepycat.persist.model.AnnotationModel;
43import com.sleepycat.persist.model.ClassMetadata;
44import com.sleepycat.persist.model.EntityMetadata;
45import com.sleepycat.persist.model.EntityModel;
46import com.sleepycat.persist.raw.RawObject;
47import com.sleepycat.util.RuntimeExceptionWrapper;
48
49/**
50 * The catalog of class formats for a store, along with its associated model
51 * and mutations.
52 *
53 * @author Mark Hayes
54 */
55public class PersistCatalog implements Catalog {
56
57    /**
58     * Key to Data record in the catalog database.  In the JE 3.0.12 beta
59     * version the formatList record is stored under this key and is converted
60     * to a Data object when it is read.
61     */
62    private static final byte[] DATA_KEY = getIntBytes(-1);
63
64    /**
65     * Key to a JE 3.0.12 beta version mutations record in the catalog
66     * database.  This record is no longer used because mutations are stored in
67     * the Data record and is deleted when the beta version is detected.
68     */
69    private static final byte[] BETA_MUTATIONS_KEY = getIntBytes(-2);
70
71    private static byte[] getIntBytes(int val) {
72        DatabaseEntry entry = new DatabaseEntry();
73        IntegerBinding.intToEntry(val, entry);
74        assert entry.getSize() == 4 && entry.getData().length == 4;
75        return entry.getData();
76    }
77
78    /**
79     * Used by unit tests.
80     */
81    public static boolean expectNoClassChanges;
82    public static boolean unevolvedFormatsEncountered;
83
84    /**
85     * The object stored under DATA_KEY in the catalog database.
86     */
87    private static class Data implements Serializable {
88
89        static final long serialVersionUID = 7515058069137413261L;
90
91        List<Format> formatList;
92        Mutations mutations;
93        int version;
94    }
95
96    /**
97     * A list of all formats indexed by formatId.  Element zero is unused and
98     * null, since IDs start at one; this avoids adjusting the ID to index the
99     * list.  Some elements are null to account for predefined IDs that are not
100     * used.
101     *
102     * <p>This field, like formatMap, is volatile because it is reassigned
103     * when dynamically adding new formats.  See {@link getFormat(Class)}.</p>
104     */
105    private volatile List<Format> formatList;
106
107    /**
108     * A map of the current/live formats in formatList, indexed by class name.
109     *
110     * <p>This field, like formatList, is volatile because it is reassigned
111     * when dynamically adding new formats.  See {@link getFormat(Class)}.</p>
112     */
113    private volatile Map<String,Format> formatMap;
114
115    /**
116     * A map of the latest formats (includes deleted formats) in formatList,
117     * indexed by class name.
118     *
119     * <p>This field, like formatMap, is volatile because it is reassigned
120     * when dynamically adding new formats.  See {@link getFormat(Class)}.</p>
121     */
122    private volatile Map<String,Format> latestFormatMap;
123
124    /**
125     * A temporary map of proxied class name to proxy class name.  Used during
126     * catalog creation, and then set to null.  This map is used to force proxy
127     * formats to be created prior to proxied formats. [#14665]
128     */
129    private Map<String,String> proxyClassMap;
130
131    private boolean rawAccess;
132    private EntityModel model;
133    private Mutations mutations;
134    private Database db;
135    private int openCount;
136
137    /**
138     * The Store is normally present but may be null in unit tests (for
139     * example, BindingTest).
140     */
141    private Store store;
142
143    /**
144     * The Evolver and catalog Data are non-null during catalog initialization,
145     * and null otherwise.
146     */
147    private Evolver evolver;
148    private Data catalogData;
149
150    /**
151     * Creates a new catalog, opening the database and reading it from a given
152     * catalog database if it already exists.  All predefined formats and
153     * formats for the given model are added.  For modified classes, old
154     * formats are defined based on the rules for compatible class changes and
155     * the given mutations.  If any format is changed or added, and the
156     * database is not read-only, write the initialized catalog to the
157     * database.
158     */
159    public PersistCatalog(Transaction txn,
160                          Environment env,
161                          String storePrefix,
162                          String dbName,
163                          DatabaseConfig dbConfig,
164                          EntityModel modelParam,
165                          Mutations mutationsParam,
166                          boolean rawAccess,
167                          Store store)
168        throws DatabaseException {
169
170        this.rawAccess = rawAccess;
171        this.store = store;
172        /* store may be null for testing. */
173        String[] fileAndDbNames = (store != null) ?
174            store.parseDbName(dbName) :
175            Store.parseDbName(dbName, DatabaseNamer.DEFAULT);
176        try {
177            db = DbCompat.openDatabase
178                (env, txn, fileAndDbNames[0], fileAndDbNames[1],
179                 dbConfig);
180        } catch (FileNotFoundException e) {
181            throw new DatabaseException(e);
182        }
183        openCount = 1;
184        boolean success = false;
185        try {
186            catalogData = readData(txn);
187            mutations = catalogData.mutations;
188            if (mutations == null) {
189                mutations = new Mutations();
190            }
191
192            /*
193             * When the beta version is detected, force a re-write of the
194             * catalog and disallow class changes.  This brings the catalog up
195             * to date so that evolution can proceed correctly from then on.
196             */
197            boolean betaVersion = (catalogData.version == BETA_VERSION);
198            boolean forceWriteData = betaVersion;
199            boolean disallowClassChanges = betaVersion;
200
201            /*
202             * Store the given mutations if they are different from the stored
203             * mutations, and force evolution to apply the new mutations.
204             */
205            boolean forceEvolution = false;
206            if (mutationsParam != null &&
207                !mutations.equals(mutationsParam)) {
208                mutations = mutationsParam;
209                forceWriteData = true;
210                forceEvolution = true;
211            }
212
213            /* Get the existing format list, or copy it from SimpleCatalog. */
214            formatList = catalogData.formatList;
215            if (formatList == null) {
216                formatList = SimpleCatalog.copyFormatList();
217
218                /*
219                 * Special cases: Object and Number are predefined but are not
220                 * simple types.
221                 */
222                Format format = new NonPersistentFormat(Object.class);
223                format.setId(Format.ID_OBJECT);
224                formatList.set(Format.ID_OBJECT, format);
225                format = new NonPersistentFormat(Number.class);
226                format.setId(Format.ID_NUMBER);
227                formatList.set(Format.ID_NUMBER, format);
228            } else {
229                if (SimpleCatalog.copyMissingFormats(formatList)) {
230                    forceWriteData = true;
231                }
232            }
233
234            /* Special handling for JE 3.0.12 beta formats. */
235            if (betaVersion) {
236                Map<String,Format> formatMap = new HashMap<String,Format>();
237                for (Format format : formatList) {
238                    if (format != null) {
239                        formatMap.put(format.getClassName(), format);
240                    }
241                }
242                for (Format format : formatList) {
243                    if (format != null) {
244                        format.migrateFromBeta(formatMap);
245                    }
246                }
247            }
248
249            /*
250             * If we should not use the current model, initialize the stored
251             * model and return.
252             */
253            formatMap = new HashMap<String,Format>(formatList.size());
254            latestFormatMap = new HashMap<String,Format>(formatList.size());
255            if (rawAccess) {
256                for (Format format : formatList) {
257                    if (format != null) {
258                        String name = format.getClassName();
259                        if (format.isCurrentVersion()) {
260                            formatMap.put(name, format);
261                        }
262                        if (format == format.getLatestVersion()) {
263                            latestFormatMap.put(name, format);
264                        }
265                    }
266                }
267                for (Format format : formatList) {
268                    if (format != null) {
269                        format.initializeIfNeeded(this);
270                    }
271                }
272                model = new StoredModel(this);
273                success = true;
274                return;
275            }
276
277            /*
278             * We are opening a store that uses the current model. Default to
279             * the AnnotationModel if no model is specified.
280             */
281            if (modelParam != null) {
282                model = modelParam;
283            } else {
284                model = new AnnotationModel();
285            }
286
287            /*
288             * Add all predefined (simple) formats to the format map.  The
289             * current version of other formats will be added below.
290             */
291            for (int i = 0; i <= Format.ID_PREDEFINED; i += 1) {
292                Format simpleFormat = formatList.get(i);
293                if (simpleFormat != null) {
294                    formatMap.put(simpleFormat.getClassName(), simpleFormat);
295                }
296            }
297
298            /*
299             * Known classes are those explicitly registered by the user via
300             * the model, plus the predefined proxy classes.
301             */
302            List<String> knownClasses =
303                new ArrayList<String>(model.getKnownClasses());
304            addPredefinedProxies(knownClasses);
305
306            /*
307             * Create a temporary map of proxied class name to proxy class
308             * name, using all known formats and classes.  This map is used to
309             * force proxy formats to be created prior to proxied formats.
310             * [#14665]
311             */
312            proxyClassMap = new HashMap<String,String>();
313            for (Format oldFormat : formatList) {
314                if (oldFormat == null || Format.isPredefined(oldFormat)) {
315                    continue;
316                }
317                String oldName = oldFormat.getClassName();
318                Renamer renamer = mutations.getRenamer
319                    (oldName, oldFormat.getVersion(), null);
320                String newName =
321                    (renamer != null) ? renamer.getNewName() : oldName;
322                addProxiedClass(newName);
323            }
324            for (String className : knownClasses) {
325                addProxiedClass(className);
326            }
327
328            /*
329             * Add known formats from the model and the predefined proxies.
330             * In general, classes will not be present in an AnnotationModel
331             * until an instance is stored, in which case an old format exists.
332             * However, registered proxy classes are an exception and must be
333             * added in advance.  And the user may choose to register new
334             * classes in advance.  The more formats we define in advance, the
335             * less times we have to write to the catalog database.
336             */
337            Map<String,Format> newFormats = new HashMap<String,Format>();
338            for (String className : knownClasses) {
339                createFormat(className, newFormats);
340            }
341
342            /*
343             * Perform class evolution for all old formats, and throw an
344             * exception that contains the messages for all of the errors in
345             * mutations or in the definition of new classes.
346             */
347            evolver = new Evolver
348                (this, storePrefix, mutations, newFormats, forceEvolution,
349                 disallowClassChanges);
350            for (Format oldFormat : formatList) {
351                if (oldFormat == null || Format.isPredefined(oldFormat)) {
352                    continue;
353                }
354                if (oldFormat.isEntity()) {
355                    evolver.evolveFormat(oldFormat);
356                } else {
357                    evolver.addNonEntityFormat(oldFormat);
358                }
359            }
360            evolver.finishEvolution();
361            String errors = evolver.getErrors();
362            if (errors != null) {
363                throw new IncompatibleClassException(errors);
364            }
365
366            /*
367             * Add the new formats remaining.  New formats that are equal to
368             * old formats were removed from the newFormats map above.
369             */
370            for (Format newFormat : newFormats.values()) {
371                addFormat(newFormat);
372            }
373
374            /* Initialize all formats. */
375            for (Format format : formatList) {
376                if (format != null) {
377                    format.initializeIfNeeded(this);
378                    if (format == format.getLatestVersion()) {
379                        latestFormatMap.put(format.getClassName(), format);
380                    }
381                }
382            }
383
384            boolean needWrite =
385                 newFormats.size() > 0 ||
386                 evolver.areFormatsChanged();
387
388            /* For unit testing. */
389            if (expectNoClassChanges && needWrite) {
390                throw new IllegalStateException
391                    ("Unexpected changes " +
392                     " newFormats.size=" + newFormats.size() +
393                     " areFormatsChanged=" + evolver.areFormatsChanged());
394            }
395
396            /* Write the catalog if anything changed. */
397            if ((needWrite || forceWriteData) &&
398                !db.getConfig().getReadOnly()) {
399
400                /*
401                 * Only rename/remove databases if we are going to update the
402                 * catalog to reflect those class changes.
403                 */
404                evolver.renameAndRemoveDatabases(store, txn);
405
406                /*
407                 * Note that we use the Data object that was read above, and
408                 * the beta version determines whether to delete the old
409                 * mutations record.
410                 */
411                catalogData.formatList = formatList;
412                catalogData.mutations = mutations;
413                writeData(txn, catalogData);
414            } else if (forceWriteData) {
415                throw new IllegalArgumentException
416                    ("When an upgrade is required the store may not be " +
417                     "opened read-only");
418            }
419
420            success = true;
421        } finally {
422
423            /*
424             * Fields needed only for the duration of this ctor and which
425             * should be null afterwards.
426             */
427            proxyClassMap = null;
428            catalogData = null;
429            evolver = null;
430
431            if (!success) {
432                close();
433            }
434        }
435    }
436
437    public void getEntityFormats(Collection<Format> entityFormats) {
438        for (Format format : formatMap.values()) {
439            if (format.isEntity()) {
440                entityFormats.add(format);
441            }
442        }
443    }
444
445    private void addProxiedClass(String className) {
446        ClassMetadata metadata = model.getClassMetadata(className);
447        if (metadata != null) {
448            String proxiedClassName = metadata.getProxiedClassName();
449            if (proxiedClassName != null) {
450                proxyClassMap.put(proxiedClassName, className);
451            }
452        }
453    }
454
455    private void addPredefinedProxies(List<String> knownClasses) {
456        knownClasses.add(CollectionProxy.ArrayListProxy.class.getName());
457        knownClasses.add(CollectionProxy.LinkedListProxy.class.getName());
458        knownClasses.add(CollectionProxy.HashSetProxy.class.getName());
459        knownClasses.add(CollectionProxy.TreeSetProxy.class.getName());
460        knownClasses.add(MapProxy.HashMapProxy.class.getName());
461        knownClasses.add(MapProxy.TreeMapProxy.class.getName());
462    }
463
464    /**
465     * Returns a map from format to a set of its superclass formats.  The
466     * format for simple types, enums and class Object are not included.  Only
467     * complex types have superclass formats as defined by
468     * Format.getSuperFormat.
469     */
470    Map<Format,Set<Format>> getSubclassMap() {
471        Map<Format,Set<Format>> subclassMap =
472            new HashMap<Format,Set<Format>>();
473        for (Format format : formatList) {
474            if (format == null || Format.isPredefined(format)) {
475                continue;
476            }
477            Format superFormat = format.getSuperFormat();
478            if (superFormat != null) {
479                Set<Format> subclass = subclassMap.get(superFormat);
480                if (subclass == null) {
481                    subclass = new HashSet<Format>();
482                    subclassMap.put(superFormat, subclass);
483                }
484                subclass.add(format);
485            }
486        }
487        return subclassMap;
488    }
489
490    /**
491     * Returns the model parameter, default model or stored model.
492     */
493    public EntityModel getResolvedModel() {
494        return model;
495    }
496
497    /**
498     * Increments the reference count for a catalog that is already open.
499     */
500    public void openExisting() {
501        openCount += 1;
502    }
503
504    /**
505     * Decrements the reference count and closes the catalog DB when it reaches
506     * zero.  Returns true if the database was closed or false if the reference
507     * count is still non-zero and the database was left open.
508     */
509    public boolean close()
510        throws DatabaseException {
511
512        if (openCount == 0) {
513            throw new IllegalStateException("Catalog is not open");
514        } else {
515            openCount -= 1;
516            if (openCount == 0) {
517                Database dbToClose = db;
518                db = null;
519                dbToClose.close();
520                return true;
521            } else {
522                return false;
523            }
524        }
525    }
526
527    /**
528     * Returns the current merged mutations.
529     */
530    public Mutations getMutations() {
531        return mutations;
532    }
533
534    /**
535     * Convenience method that gets the class for the given class name and
536     * calls createFormat with the class object.
537     */
538    public Format createFormat(String clsName, Map<String,Format> newFormats) {
539        Class type;
540        try {
541            type = SimpleCatalog.classForName(clsName);
542        } catch (ClassNotFoundException e) {
543            throw new IllegalStateException
544                ("Class does not exist: " + clsName);
545        }
546        return createFormat(type, newFormats);
547    }
548
549    /**
550     * If the given class format is not already present in the given map,
551     * creates an uninitialized format, adds it to the map, and also collects
552     * related formats in the map.
553     */
554    public Format createFormat(Class type, Map<String,Format> newFormats) {
555        /* Return a new or existing format for this class. */
556        String className = type.getName();
557        Format format = newFormats.get(className);
558        if (format != null) {
559            return format;
560        }
561        format = formatMap.get(className);
562        if (format != null) {
563            return format;
564        }
565        /* Simple types are predefined. */
566        assert !SimpleCatalog.isSimpleType(type) : className;
567        /* Create format of the appropriate type. */
568        String proxyClassName = null;
569        if (proxyClassMap != null) {
570            proxyClassName = proxyClassMap.get(className);
571        }
572        if (proxyClassName != null) {
573            format = new ProxiedFormat(type, proxyClassName);
574        } else if (type.isArray()) {
575            format = type.getComponentType().isPrimitive() ?
576                (new PrimitiveArrayFormat(type)) :
577                (new ObjectArrayFormat(type));
578        } else if (type.isEnum()) {
579            format = new EnumFormat(type);
580        } else if (type == Object.class || type.isInterface()) {
581            format = new NonPersistentFormat(type);
582        } else {
583            ClassMetadata metadata = model.getClassMetadata(className);
584            if (metadata == null) {
585                throw new IllegalArgumentException
586                    ("Class could not be loaded or is not persistent: " +
587                     className);
588            }
589            if (metadata.getCompositeKeyFields() != null &&
590                (metadata.getPrimaryKey() != null ||
591                 metadata.getSecondaryKeys() != null)) {
592                throw new IllegalArgumentException
593                    ("A composite key class may not have primary or" +
594                     " secondary key fields: " + type.getName());
595            }
596            try {
597                type.getDeclaredConstructor();
598            } catch (NoSuchMethodException e) {
599                throw new IllegalArgumentException
600                    ("No default constructor: " + type.getName(), e);
601            }
602            if (metadata.getCompositeKeyFields() != null) {
603                format = new CompositeKeyFormat
604                    (type, metadata, metadata.getCompositeKeyFields());
605            } else {
606                EntityMetadata entityMetadata =
607                    model.getEntityMetadata(className);
608                format = new ComplexFormat(type, metadata, entityMetadata);
609            }
610        }
611        /* Collect new format along with any related new formats. */
612        newFormats.put(className, format);
613        format.collectRelatedFormats(this, newFormats);
614
615        return format;
616    }
617
618    /**
619     * Adds a format and makes it the current format for the class.
620     */
621    private void addFormat(Format format) {
622        addFormat(format, formatList, formatMap);
623    }
624
625    /**
626     * Adds a format to the given the format collections, for use when
627     * dynamically adding formats.
628     */
629    private void addFormat(Format format,
630                           List<Format> list,
631                           Map<String,Format> map) {
632        format.setId(list.size());
633        list.add(format);
634        map.put(format.getClassName(), format);
635    }
636
637    /**
638     * Installs an existing format when no evolution is needed, i.e, when the
639     * new and old formats are identical.
640     */
641    void useExistingFormat(Format oldFormat) {
642        assert oldFormat.isCurrentVersion();
643        formatMap.put(oldFormat.getClassName(), oldFormat);
644    }
645
646    /**
647     * Returns a set of all persistent (non-simple type) class names.
648     */
649    Set<String> getModelClasses() {
650        Set<String> classes = new HashSet<String>();
651        for (Format format : formatMap.values()) {
652            if (format.isModelClass()) {
653                classes.add(format.getClassName());
654            }
655        }
656        return classes;
657    }
658
659    /**
660     * When a format is intialized, this method is called to get the version
661     * of the serialized object to be initialized.  See Catalog.
662     */
663    public int getInitVersion(Format format, boolean forReader) {
664
665        if (catalogData == null || catalogData.formatList == null ||
666            format.getId() >= catalogData.formatList.size()) {
667
668            /*
669             * For new formats, use the current version.  If catalogData is
670             * null, the Catalog ctor is finished and the format must be new.
671             * If the ctor is in progress, the format is new if its ID is
672             * greater than the ID of all pre-existing formats.
673             */
674            return Catalog.CURRENT_VERSION;
675        } else {
676
677            /*
678             * Get the version of a pre-existing format during execution of the
679             * Catalog ctor.  The catalogData field is non-null, but evolver
680             * may be null if the catalog is opened in raw mode.
681             */
682            assert catalogData != null;
683
684            if (forReader) {
685
686                /*
687                 * Get the version of the evolution reader for a pre-existing
688                 * format.  Use the current version if the format changed
689                 * during class evolution, otherwise use the stored version.
690                 */
691                return (evolver != null && evolver.isFormatChanged(format)) ?
692                       Catalog.CURRENT_VERSION : catalogData.version;
693            } else {
694                /* Always used the stored version for a pre-existing format. */
695                return catalogData.version;
696            }
697        }
698    }
699
700    public Format getFormat(int formatId) {
701        try {
702            Format format = formatList.get(formatId);
703            if (format == null) {
704                throw new DeletedClassException
705                    ("Format does not exist: " + formatId);
706            }
707            return format;
708        } catch (NoSuchElementException e) {
709            throw new DeletedClassException
710                ("Format does not exist: " + formatId);
711        }
712    }
713
714
715    /**
716     * Get a format for a given class, creating it if it does not exist.
717     *
718     * <p>This method is called for top level entity instances by
719     * PersistEntityBinding.  When a new entity subclass format is added we
720     * call Store.openSecondaryIndexes so that previously unknown secondary
721     * databases can be created, before storing the entity.  We do this here
722     * while not holding a synchronization mutex, not in addNewFormat, to avoid
723     * deadlocks. openSecondaryIndexes synchronizes on the Store. [#15247]</p>
724     */
725    public Format getFormat(Class cls) {
726        Format format = formatMap.get(cls.getName());
727        if (format == null) {
728            if (model != null) {
729                format = addNewFormat(cls);
730                /* Detect and handle new entity subclass. [#15247] */
731                if (store != null) {
732                    Format entityFormat = format.getEntityFormat();
733                    if (entityFormat != null && entityFormat != format) {
734                        try {
735                            store.openSecondaryIndexes
736                                (null, entityFormat.getEntityMetadata(), null);
737                        } catch (DatabaseException e) {
738                            throw new RuntimeExceptionWrapper(e);
739                        }
740                    }
741                }
742            }
743            if (format == null) {
744                throw new IllegalArgumentException
745                    ("Class is not persistent: " + cls.getName());
746            }
747        }
748        return format;
749    }
750
751    public Format getFormat(String className) {
752        return formatMap.get(className);
753    }
754
755    public Format getLatestVersion(String className) {
756        return latestFormatMap.get(className);
757    }
758
759    /**
760     * Adds a format for a new class.  Returns the format added for the given
761     * class, or throws an exception if the given class is not persistent.
762     *
763     * <p>This method uses a copy-on-write technique to add new formats without
764     * impacting other threads.</p>
765     */
766    private synchronized Format addNewFormat(Class cls) {
767
768        /*
769         * After synchronizing, check whether another thread has added the
770         * format needed.  Note that this is not the double-check technique
771         * because the formatMap field is volatile and is not itself checked
772         * for null.  (The double-check technique is known to be flawed in
773         * Java.)
774         */
775        Format format = formatMap.get(cls.getName());
776        if (format != null) {
777            return format;
778        }
779
780        /* Copy the read-only format collections. */
781        List<Format> newFormatList = new ArrayList<Format>(formatList);
782        Map<String,Format> newFormatMap =
783            new HashMap<String,Format>(formatMap);
784        Map<String,Format> newLatestFormatMap =
785            new HashMap<String,Format>(latestFormatMap);
786
787        /* Add the new format and all related new formats. */
788        Map<String,Format> newFormats = new HashMap<String,Format>();
789        format = createFormat(cls, newFormats);
790        for (Format newFormat : newFormats.values()) {
791            addFormat(newFormat, newFormatList, newFormatMap);
792        }
793
794        /*
795         * Initialize new formats using a read-only catalog because we can't
796         * update this catalog until after we store it (below).
797         */
798        Catalog newFormatCatalog =
799            new ReadOnlyCatalog(newFormatList, newFormatMap);
800        for (Format newFormat : newFormats.values()) {
801            newFormat.initializeIfNeeded(newFormatCatalog);
802            newLatestFormatMap.put(newFormat.getClassName(), newFormat);
803        }
804
805        /*
806         * Write the updated catalog using auto-commit, then assign the new
807         * collections.  The database write must occur before the collections
808         * are used, since a format must be persistent before it can be
809         * referenced by a data record.
810         */
811        try {
812            Data catalogData = new Data();
813            catalogData.formatList = newFormatList;
814            catalogData.mutations = mutations;
815            writeData(null, catalogData);
816        } catch (DatabaseException e) {
817            throw new RuntimeExceptionWrapper(e);
818        }
819        formatList = newFormatList;
820        formatMap = newFormatMap;
821        latestFormatMap = newLatestFormatMap;
822
823        return format;
824    }
825
826    /**
827     * Used to write the catalog when a format has been changed, for example,
828     * when Store.evolve has updated a Format's EvolveNeeded property.  Uses
829     * auto-commit.
830     */
831    public synchronized void flush()
832        throws DatabaseException {
833
834        Data catalogData = new Data();
835        catalogData.formatList = formatList;
836        catalogData.mutations = mutations;
837        writeData(null, catalogData);
838    }
839
840    /**
841     * Reads catalog Data, converting old versions as necessary.  An empty
842     * Data object is returned if no catalog data currently exists.  Null is
843     * never returned.
844     */
845    private Data readData(Transaction txn)
846        throws DatabaseException {
847
848        Data catalogData;
849        DatabaseEntry key = new DatabaseEntry(DATA_KEY);
850        DatabaseEntry data = new DatabaseEntry();
851        OperationStatus status = db.get(txn, key, data, null);
852        if (status == OperationStatus.SUCCESS) {
853            ByteArrayInputStream bais = new ByteArrayInputStream
854                (data.getData(), data.getOffset(), data.getSize());
855            try {
856                ObjectInputStream ois = new ObjectInputStream(bais);
857                Object object = ois.readObject();
858                assert ois.available() == 0;
859                if (object instanceof Data) {
860                    catalogData = (Data) object;
861                } else {
862                    if (!(object instanceof List)) {
863                        throw new IllegalStateException
864                            (object.getClass().getName());
865                    }
866                    catalogData = new Data();
867                    catalogData.formatList = (List) object;
868                    catalogData.version = BETA_VERSION;
869                }
870                return catalogData;
871            } catch (ClassNotFoundException e) {
872                throw new DatabaseException(e);
873            } catch (IOException e) {
874                throw new DatabaseException(e);
875            }
876        } else {
877            catalogData = new Data();
878            catalogData.version = Catalog.CURRENT_VERSION;
879        }
880        return catalogData;
881    }
882
883    /**
884     * Writes catalog Data.  If txn is null, auto-commit is used.
885     */
886    private void writeData(Transaction txn, Data catalogData)
887        throws DatabaseException {
888
889        /* Catalog data is written in the current version. */
890        boolean wasBetaVersion = (catalogData.version == BETA_VERSION);
891        catalogData.version = CURRENT_VERSION;
892
893        ByteArrayOutputStream baos = new ByteArrayOutputStream();
894        try {
895            ObjectOutputStream oos = new ObjectOutputStream(baos);
896            oos.writeObject(catalogData);
897        } catch (IOException e) {
898            throw new DatabaseException(e);
899        }
900        DatabaseEntry key = new DatabaseEntry(DATA_KEY);
901        DatabaseEntry data = new DatabaseEntry(baos.toByteArray());
902        db.put(txn, key, data);
903
904        /*
905         * Delete the unused beta mutations record if we read the beta version
906         * record earlier.
907         */
908        if (wasBetaVersion) {
909            key.setData(BETA_MUTATIONS_KEY);
910            db.delete(txn, key);
911        }
912    }
913
914    public boolean isRawAccess() {
915        return rawAccess;
916    }
917
918    public Object convertRawObject(RawObject o, IdentityHashMap converted) {
919        Format format = (Format) o.getType();
920        if (this != format.getCatalog()) {
921	    String className = format.getClassName();
922            format = getFormat(className);
923            if (format == null) {
924                throw new IllegalArgumentException
925                    ("External raw type not found: " + className);
926            }
927        }
928        Format proxiedFormat = format.getProxiedFormat();
929        if (proxiedFormat != null) {
930            format = proxiedFormat;
931        }
932        if (converted == null) {
933            converted = new IdentityHashMap();
934        }
935        return format.convertRawObject(this, false, o, converted);
936    }
937
938    public void dump() {
939        System.out.println("--- Begin formats ---");
940        for (Format format : formatList) {
941            if (format != null) {
942                System.out.println
943                    ("ID: " + format.getId() +
944                     " class: " + format.getClassName() +
945                     " version: " + format.getVersion() +
946                     " current: " +
947                     (format == formatMap.get(format.getClassName())));
948            }
949        }
950        System.out.println("--- End formats ---");
951    }
952}
953