1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2000,2008 Oracle.  All rights reserved.
5 *
6 * $Id: StoredClassCatalog.java,v 12.7 2008/01/08 20:58:35 bostic Exp $
7 */
8
9package com.sleepycat.bind.serial;
10
11import java.io.ByteArrayInputStream;
12import java.io.ByteArrayOutputStream;
13import java.io.IOException;
14import java.io.ObjectInputStream;
15import java.io.ObjectOutputStream;
16import java.io.ObjectStreamClass;
17import java.io.Serializable;
18import java.math.BigInteger;
19import java.util.HashMap;
20
21import com.sleepycat.compat.DbCompat;
22import com.sleepycat.db.Cursor;
23import com.sleepycat.db.CursorConfig;
24import com.sleepycat.db.Database;
25import com.sleepycat.db.DatabaseConfig;
26import com.sleepycat.db.DatabaseEntry;
27import com.sleepycat.db.DatabaseException;
28import com.sleepycat.db.EnvironmentConfig;
29import com.sleepycat.db.LockMode;
30import com.sleepycat.db.OperationStatus;
31import com.sleepycat.db.Transaction;
32import com.sleepycat.util.RuntimeExceptionWrapper;
33import com.sleepycat.util.UtfOps;
34
35/**
36 * A <code>ClassCatalog</code> that is stored in a <code>Database</code>.
37 *
38 * <p>A single <code>StoredClassCatalog</code> object is normally used along
39 * with a set of databases that stored serialized objects.</p>
40 *
41 * @author Mark Hayes
42 */
43public class StoredClassCatalog implements ClassCatalog {
44
45    /*
46     * Record types ([key] [data]):
47     *
48     * [0] [next class ID]
49     * [1 / class ID] [ObjectStreamClass (class format)]
50     * [2 / class name] [ClassInfo (has 8 byte class ID)]
51     */
52    private static final byte REC_LAST_CLASS_ID = (byte) 0;
53    private static final byte REC_CLASS_FORMAT = (byte) 1;
54    private static final byte REC_CLASS_INFO = (byte) 2;
55
56    private static final byte[] LAST_CLASS_ID_KEY = {REC_LAST_CLASS_ID};
57
58    private Database db;
59    private HashMap classMap;
60    private HashMap formatMap;
61    private LockMode writeLockMode;
62    private boolean cdbMode;
63    private boolean txnMode;
64
65    /**
66     * Creates a catalog based on a given database. To save resources, only a
67     * single catalog object should be used for each unique catalog database.
68     *
69     * @param database an open database to use as the class catalog.  It must
70     * be a BTREE database and must not allow duplicates.
71     *
72     * @throws DatabaseException if an error occurs accessing the database.
73     *
74     * @throws IllegalArgumentException if the database is not a BTREE database
75     * or if it configured to allow duplicates.
76     */
77    public StoredClassCatalog(Database database)
78        throws DatabaseException, IllegalArgumentException {
79
80        db = database;
81        DatabaseConfig dbConfig = db.getConfig();
82        EnvironmentConfig envConfig = db.getEnvironment().getConfig();
83
84        writeLockMode = (DbCompat.getInitializeLocking(envConfig) ||
85                         envConfig.getTransactional()) ? LockMode.RMW
86                                                       : LockMode.DEFAULT;
87        cdbMode = DbCompat.getInitializeCDB(envConfig);
88        txnMode = dbConfig.getTransactional();
89
90        if (!DbCompat.isTypeBtree(dbConfig)) {
91            throw new IllegalArgumentException(
92                    "The class catalog must be a BTREE database.");
93        }
94        if (DbCompat.getSortedDuplicates(dbConfig) ||
95            DbCompat.getUnsortedDuplicates(dbConfig)) {
96            throw new IllegalArgumentException(
97                    "The class catalog database must not allow duplicates.");
98        }
99
100        /*
101         * Create the class format and class info maps. Note that these are not
102         * synchronized, and therefore the methods that use them are
103         * synchronized.
104         */
105        classMap = new HashMap();
106        formatMap = new HashMap();
107
108        DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
109        DatabaseEntry data = new DatabaseEntry();
110        if (dbConfig.getReadOnly()) {
111            /* Check that the class ID record exists. */
112            OperationStatus status = db.get(null, key, data, null);
113            if (status != OperationStatus.SUCCESS) {
114                throw new IllegalStateException
115                    ("A read-only catalog database may not be empty");
116            }
117        } else {
118            /* Add the initial class ID record if it doesn't exist.  */
119            data.setData(new byte[1]); // zero ID
120            /* Use putNoOverwrite to avoid phantoms. */
121            db.putNoOverwrite(null, key, data);
122        }
123    }
124
125    // javadoc is inherited
126    public synchronized void close()
127        throws DatabaseException {
128
129        if (db != null) {
130            db.close();
131        }
132        db = null;
133        formatMap = null;
134        classMap = null;
135    }
136
137    // javadoc is inherited
138    public synchronized byte[] getClassID(ObjectStreamClass classFormat)
139        throws DatabaseException, ClassNotFoundException {
140
141        ClassInfo classInfo = getClassInfo(classFormat);
142        return classInfo.getClassID();
143    }
144
145    // javadoc is inherited
146    public synchronized ObjectStreamClass getClassFormat(byte[] classID)
147        throws DatabaseException, ClassNotFoundException {
148
149        return getClassFormat(classID, new DatabaseEntry());
150    }
151
152    /**
153     * Internal function for getting the class format.  Allows passing the
154     * DatabaseEntry object for the data, so the bytes of the class format can
155     * be examined afterwards.
156     */
157    private ObjectStreamClass getClassFormat(byte[] classID,
158					     DatabaseEntry data)
159        throws DatabaseException, ClassNotFoundException {
160
161        /* First check the map and, if found, add class info to the map. */
162
163        BigInteger classIDObj = new BigInteger(classID);
164        ObjectStreamClass classFormat =
165            (ObjectStreamClass) formatMap.get(classIDObj);
166        if (classFormat == null) {
167
168            /* Make the class format key. */
169
170            byte[] keyBytes = new byte[classID.length + 1];
171            keyBytes[0] = REC_CLASS_FORMAT;
172            System.arraycopy(classID, 0, keyBytes, 1, classID.length);
173            DatabaseEntry key = new DatabaseEntry(keyBytes);
174
175            /* Read the class format. */
176
177            OperationStatus status = db.get(null, key, data, LockMode.DEFAULT);
178            if (status != OperationStatus.SUCCESS) {
179                throw new ClassNotFoundException("Catalog class ID not found");
180            }
181            try {
182                ObjectInputStream ois =
183                    new ObjectInputStream(
184                        new ByteArrayInputStream(data.getData(),
185                                                 data.getOffset(),
186                                                 data.getSize()));
187                classFormat = (ObjectStreamClass) ois.readObject();
188            } catch (IOException e) {
189                throw new RuntimeExceptionWrapper(e);
190            }
191
192            /* Update the class format map. */
193
194            formatMap.put(classIDObj, classFormat);
195        }
196        return classFormat;
197    }
198
199    /**
200     * Get the ClassInfo for a given class name, adding it and its
201     * ObjectStreamClass to the database if they are not already present, and
202     * caching both of them using the class info and class format maps.  When a
203     * class is first loaded from the database, the stored ObjectStreamClass is
204     * compared to the current ObjectStreamClass loaded by the Java class
205     * loader; if they are different, a new class ID is assigned for the
206     * current format.
207     */
208    private ClassInfo getClassInfo(ObjectStreamClass classFormat)
209        throws DatabaseException, ClassNotFoundException {
210
211        /*
212         * First check for a cached copy of the class info, which if
213         * present always contains the class format object
214         */
215        String className = classFormat.getName();
216        ClassInfo classInfo = (ClassInfo) classMap.get(className);
217        if (classInfo != null) {
218            return classInfo;
219        } else {
220            /* Make class info key.  */
221            char[] nameChars = className.toCharArray();
222            byte[] keyBytes = new byte[1 + UtfOps.getByteLength(nameChars)];
223            keyBytes[0] = REC_CLASS_INFO;
224            UtfOps.charsToBytes(nameChars, 0, keyBytes, 1, nameChars.length);
225            DatabaseEntry key = new DatabaseEntry(keyBytes);
226
227            /* Read class info.  */
228            DatabaseEntry data = new DatabaseEntry();
229            OperationStatus status = db.get(null, key, data, LockMode.DEFAULT);
230            if (status != OperationStatus.SUCCESS) {
231                /*
232                 * Not found in the database, write class info and class
233                 * format.
234                 */
235                classInfo = putClassInfo(new ClassInfo(), className, key,
236                                         classFormat);
237            } else {
238                /*
239                 * Read class info to get the class format key, then read class
240                 * format.
241                 */
242                classInfo = new ClassInfo(data);
243                DatabaseEntry formatData = new DatabaseEntry();
244                ObjectStreamClass storedClassFormat =
245                    getClassFormat(classInfo.getClassID(), formatData);
246
247                /*
248                 * Compare the stored class format to the current class format,
249                 * and if they are different then generate a new class ID.
250                 */
251                if (!areClassFormatsEqual(storedClassFormat,
252                                          getBytes(formatData),
253                                          classFormat)) {
254                    classInfo = putClassInfo(classInfo, className, key,
255                                             classFormat);
256                }
257
258                /* Update the class info map.  */
259                classInfo.setClassFormat(classFormat);
260                classMap.put(className, classInfo);
261            }
262        }
263        return classInfo;
264    }
265
266    /**
267     * Assign a new class ID (increment the current ID record), write the
268     * ObjectStreamClass record for this new ID, and update the ClassInfo
269     * record with the new ID also.  The ClassInfo passed as an argument is the
270     * one to be updated.
271     */
272    private ClassInfo putClassInfo(ClassInfo classInfo,
273				   String className,
274				   DatabaseEntry classKey,
275				   ObjectStreamClass classFormat)
276        throws DatabaseException, ClassNotFoundException {
277
278        /* An intent-to-write cursor is needed for CDB. */
279        CursorConfig cursorConfig = null;
280        if (cdbMode) {
281            cursorConfig = new CursorConfig();
282            DbCompat.setWriteCursor(cursorConfig, true);
283        }
284        Cursor cursor = null;
285        Transaction txn = null;
286        try {
287            if (txnMode) {
288                txn = db.getEnvironment().beginTransaction(null, null);
289            }
290            cursor = db.openCursor(txn, cursorConfig);
291
292            /* Get the current class ID. */
293            DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
294            DatabaseEntry data = new DatabaseEntry();
295            OperationStatus status = cursor.getSearchKey(key, data,
296                                                         writeLockMode);
297            if (status != OperationStatus.SUCCESS) {
298                throw new IllegalStateException("Class ID not initialized");
299            }
300            byte[] idBytes = getBytes(data);
301
302            /* Increment the ID by one and write the updated record.  */
303            idBytes = incrementID(idBytes);
304            data.setData(idBytes);
305            cursor.put(key, data);
306
307            /*
308             * Write the new class format record whose key is the ID just
309             * assigned.
310             */
311            byte[] keyBytes = new byte[1 + idBytes.length];
312            keyBytes[0] = REC_CLASS_FORMAT;
313            System.arraycopy(idBytes, 0, keyBytes, 1, idBytes.length);
314            key.setData(keyBytes);
315
316            ByteArrayOutputStream baos = new ByteArrayOutputStream();
317            ObjectOutputStream oos;
318            try {
319                oos = new ObjectOutputStream(baos);
320                oos.writeObject(classFormat);
321            } catch (IOException e) {
322                throw new RuntimeExceptionWrapper(e);
323            }
324            data.setData(baos.toByteArray());
325
326            cursor.put(key, data);
327
328            /*
329             * Write the new class info record, using the key passed in; this
330             * is done last so that a reader who gets the class info record
331             * first will always find the corresponding class format record.
332             */
333            classInfo.setClassID(idBytes);
334            classInfo.toDbt(data);
335
336            cursor.put(classKey, data);
337
338            /*
339             * Update the maps before closing the cursor, so that the cursor
340             * lock prevents other writers from duplicating this entry.
341             */
342            classInfo.setClassFormat(classFormat);
343            classMap.put(className, classInfo);
344            formatMap.put(new BigInteger(idBytes), classFormat);
345            return classInfo;
346        } finally {
347            if (cursor != null) {
348                cursor.close();
349            }
350            if (txn != null) {
351                txn.commit();
352            }
353        }
354    }
355
356    private static byte[] incrementID(byte[] key) {
357
358        BigInteger id = new BigInteger(key);
359        id = id.add(BigInteger.valueOf(1));
360        return id.toByteArray();
361    }
362
363    /**
364     * Holds the class format key for a class, maintains a reference to the
365     * ObjectStreamClass.  Other fields can be added when we need to store more
366     * information per class.
367     */
368    private static class ClassInfo implements Serializable {
369
370        private byte[] classID;
371        private transient ObjectStreamClass classFormat;
372
373        ClassInfo() {
374        }
375
376        ClassInfo(DatabaseEntry dbt) {
377
378            byte[] data = dbt.getData();
379            int len = data[0];
380            classID = new byte[len];
381            System.arraycopy(data, 1, classID, 0, len);
382        }
383
384        void toDbt(DatabaseEntry dbt) {
385
386            byte[] data = new byte[1 + classID.length];
387            data[0] = (byte) classID.length;
388            System.arraycopy(classID, 0, data, 1, classID.length);
389            dbt.setData(data);
390        }
391
392        void setClassID(byte[] classID) {
393
394            this.classID = classID;
395        }
396
397        byte[] getClassID() {
398
399            return classID;
400        }
401
402        ObjectStreamClass getClassFormat() {
403
404            return classFormat;
405        }
406
407        void setClassFormat(ObjectStreamClass classFormat) {
408
409            this.classFormat = classFormat;
410        }
411    }
412
413    /**
414     * Return whether two class formats are equal.  This determines whether a
415     * new class format is needed for an object being serialized.  Formats must
416     * be identical in all respects, or a new format is needed.
417     */
418    private static boolean areClassFormatsEqual(ObjectStreamClass format1,
419                                                byte[] format1Bytes,
420                                                ObjectStreamClass format2) {
421        try {
422            if (format1Bytes == null) { // using cached format1 object
423                format1Bytes = getObjectBytes(format1);
424            }
425            byte[] format2Bytes = getObjectBytes(format2);
426            return java.util.Arrays.equals(format2Bytes, format1Bytes);
427        } catch (IOException e) { return false; }
428    }
429
430    /*
431     * We can return the same byte[] for 0 length arrays.
432     */
433    private static byte[] ZERO_LENGTH_BYTE_ARRAY = new byte[0];
434
435    private static byte[] getBytes(DatabaseEntry dbt) {
436        byte[] b = dbt.getData();
437        if (b == null) {
438            return null;
439        }
440        if (dbt.getOffset() == 0 && b.length == dbt.getSize()) {
441            return b;
442        }
443	int len = dbt.getSize();
444	if (len == 0) {
445	    return ZERO_LENGTH_BYTE_ARRAY;
446	} else {
447	    byte[] t = new byte[len];
448	    System.arraycopy(b, dbt.getOffset(), t, 0, t.length);
449	    return t;
450	}
451    }
452
453    private static byte[] getObjectBytes(Object o)
454        throws IOException {
455
456        ByteArrayOutputStream baos = new ByteArrayOutputStream();
457        ObjectOutputStream oos = new ObjectOutputStream(baos);
458        oos.writeObject(o);
459        return baos.toByteArray();
460    }
461}
462