001    /**
002     * JDBM LICENSE v1.00
003     *
004     * Redistribution and use of this software and associated documentation
005     * ("Software"), with or without modification, are permitted provided
006     * that the following conditions are met:
007     *
008     * 1. Redistributions of source code must retain copyright
009     *    statements and notices.  Redistributions must also contain a
010     *    copy of this document.
011     *
012     * 2. Redistributions in binary form must reproduce the
013     *    above copyright notice, this list of conditions and the
014     *    following disclaimer in the documentation and/or other
015     *    materials provided with the distribution.
016     *
017     * 3. The name "JDBM" must not be used to endorse or promote
018     *    products derived from this Software without prior written
019     *    permission of Cees de Groot.  For written permission,
020     *    please contact cg@cdegroot.com.
021     *
022     * 4. Products derived from this Software may not be called "JDBM"
023     *    nor may "JDBM" appear in their names without prior written
024     *    permission of Cees de Groot.
025     *
026     * 5. Due credit should be given to the JDBM Project
027     *    (http://jdbm.sourceforge.net/).
028     *
029     * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
030     * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
031     * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
032     * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
033     * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
034     * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
035     * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
036     * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
037     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
038     * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
039     * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
040     * OF THE POSSIBILITY OF SUCH DAMAGE.
041     *
042     * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
043     * Copyright 2000-2001 (C) Alex Boisvert. All Rights Reserved.
044     * Contributions are Copyright (C) 2000 by their associated contributors.
045     *
046     * $Id: BaseRecordManager.java,v 1.8 2005/06/25 23:12:32 doomdark Exp $
047     */
048    
049    package jdbm.recman;
050    
051    import java.io.IOException;
052    
053    import java.util.HashMap;
054    import java.util.Map;
055    
056    import jdbm.RecordManager;
057    import jdbm.helper.Serializer;
058    import jdbm.helper.DefaultSerializer;
059    
060    /**
061     *  This class manages records, which are uninterpreted blobs of data. The
062     *  set of operations is simple and straightforward: you communicate with
063     *  the class using long "rowids" and byte[] data blocks. Rowids are returned
064     *  on inserts and you can stash them away someplace safe to be able to get
065     *  back to them. Data blocks can be as long as you wish, and may have
066     *  lengths different from the original when updating.
067     *  <p>
068     *  Operations are synchronized, so that only one of them will happen
069     *  concurrently even if you hammer away from multiple threads. Operations
070     *  are made atomic by keeping a transaction log which is recovered after
071     *  a crash, so the operations specified by this interface all have ACID
072     *  properties.
073     *  <p>
074     *  You identify a file by just the name. The package attaches <tt>.db</tt>
075     *  for the database file, and <tt>.lg</tt> for the transaction log. The
076     *  transaction log is synchronized regularly and then restarted, so don't
077     *  worry if you see the size going up and down.
078     *
079     * @author <a href="mailto:boisvert@intalio.com">Alex Boisvert</a>
080     * @author <a href="cg@cdegroot.com">Cees de Groot</a>
081     * @version $Id: BaseRecordManager.java,v 1.8 2005/06/25 23:12:32 doomdark Exp $
082     */
083    public final class BaseRecordManager
084        implements RecordManager
085    {
086    
087        /**
088         * Underlying record file.
089         */
090        private RecordFile _file;
091    
092    
093        /**
094         * Physical row identifier manager.
095         */
096        private PhysicalRowIdManager _physMgr;
097    
098    
099        /**
100         * Logigal to Physical row identifier manager.
101         */
102        private LogicalRowIdManager _logMgr;
103    
104    
105        /**
106         * Page manager.
107         */
108        private PageManager _pageman;
109    
110    
111        /**
112         * Reserved slot for name directory.
113         */
114        public static final int NAME_DIRECTORY_ROOT = 0;
115    
116    
117        /**
118         * Static debugging flag
119         */
120        public static final boolean DEBUG = false;
121    
122        
123        /**
124         * Directory of named JDBMHashtables.  This directory is a persistent
125         * directory, stored as a Hashtable.  It can be retrived by using
126         * the NAME_DIRECTORY_ROOT.
127         */
128        private Map _nameDirectory;
129    
130    
131        /**
132         *  Creates a record manager for the indicated file
133         *
134         *  @throws IOException when the file cannot be opened or is not
135         *          a valid file content-wise.
136         */
137        public BaseRecordManager( String filename )
138            throws IOException
139        {
140            _file = new RecordFile( filename );
141            _pageman = new PageManager( _file );
142            _physMgr = new PhysicalRowIdManager( _file, _pageman );
143            _logMgr = new LogicalRowIdManager( _file, _pageman );
144        }
145    
146    
147        /**
148         *  Get the underlying Transaction Manager
149         */
150        public synchronized TransactionManager getTransactionManager()
151        {
152            checkIfClosed();
153    
154            return _file.txnMgr;
155        }
156    
157    
158        /**
159         *  Switches off transactioning for the record manager. This means
160         *  that a) a transaction log is not kept, and b) writes aren't
161         *  synch'ed after every update. This is useful when batch inserting
162         *  into a new database.
163         *  <p>
164         *  Only call this method directly after opening the file, otherwise
165         *  the results will be undefined.
166         */
167        public synchronized void disableTransactions()
168        {
169            checkIfClosed();
170    
171            _file.disableTransactions();
172        }
173    
174        
175        /**
176         *  Closes the record manager.
177         *
178         *  @throws IOException when one of the underlying I/O operations fails.
179         */
180        public synchronized void close()
181            throws IOException
182        {
183            checkIfClosed();
184    
185            _pageman.close();
186            _pageman = null;
187    
188            _file.close();
189            _file = null;
190        }
191    
192    
193        /**
194         *  Inserts a new record using standard java object serialization.
195         *
196         *  @param obj the object for the new record.
197         *  @return the rowid for the new record.
198         *  @throws IOException when one of the underlying I/O operations fails.
199         */
200        public long insert( Object obj )
201            throws IOException
202        {
203            return insert( obj, DefaultSerializer.INSTANCE );
204        }
205    
206        
207        /**
208         *  Inserts a new record using a custom serializer.
209         *
210         *  @param obj the object for the new record.
211         *  @param serializer a custom serializer
212         *  @return the rowid for the new record.
213         *  @throws IOException when one of the underlying I/O operations fails.
214         */
215        public synchronized long insert( Object obj, Serializer serializer )
216            throws IOException
217        {
218            byte[]    data;
219            long      recid;
220            Location  physRowId;
221            
222            checkIfClosed();
223    
224            data = serializer.serialize( obj );
225            physRowId = _physMgr.insert( data, 0, data.length );
226            recid = _logMgr.insert( physRowId ).toLong();
227            if ( DEBUG ) {
228                System.out.println( "BaseRecordManager.insert() recid " + recid + " length " + data.length ) ;
229            }
230            return recid;
231        }
232    
233        /**
234         *  Deletes a record.
235         *
236         *  @param recid the rowid for the record that should be deleted.
237         *  @throws IOException when one of the underlying I/O operations fails.
238         */
239        public synchronized void delete( long recid )
240            throws IOException
241        {
242            checkIfClosed();
243            if ( recid <= 0 ) {
244                throw new IllegalArgumentException( "Argument 'recid' is invalid: "
245                                                    + recid );
246            }
247    
248            if ( DEBUG ) {
249                System.out.println( "BaseRecordManager.delete() recid " + recid ) ;
250            }
251    
252            Location logRowId = new Location( recid );
253            Location physRowId = _logMgr.fetch( logRowId );
254            _physMgr.delete( physRowId );
255            _logMgr.delete( logRowId );
256        }
257    
258    
259        /**
260         *  Updates a record using standard java object serialization.
261         *
262         *  @param recid the recid for the record that is to be updated.
263         *  @param obj the new object for the record.
264         *  @throws IOException when one of the underlying I/O operations fails.
265         */
266        public void update( long recid, Object obj )
267            throws IOException
268        {
269            update( recid, obj, DefaultSerializer.INSTANCE );
270        }
271    
272        
273        /**
274         *  Updates a record using a custom serializer.
275         *
276         *  @param recid the recid for the record that is to be updated.
277         *  @param obj the new object for the record.
278         *  @param serializer a custom serializer
279         *  @throws IOException when one of the underlying I/O operations fails.
280         */
281        public synchronized void update( long recid, Object obj, Serializer serializer )
282            throws IOException
283        {
284            checkIfClosed();
285            if ( recid <= 0 ) {
286                throw new IllegalArgumentException( "Argument 'recid' is invalid: "
287                                                    + recid );
288            }
289    
290            Location logRecid = new Location( recid );
291            Location physRecid = _logMgr.fetch( logRecid );
292            
293            byte[] data = serializer.serialize( obj );
294            if ( DEBUG ) {
295                System.out.println( "BaseRecordManager.update() recid " + recid + " length " + data.length ) ;
296            }
297            
298            Location newRecid = _physMgr.update( physRecid, data, 0, data.length );
299            if ( ! newRecid.equals( physRecid ) ) {
300                _logMgr.update( logRecid, newRecid );
301            }
302        }
303    
304    
305        /**
306         *  Fetches a record using standard java object serialization.
307         *
308         *  @param recid the recid for the record that must be fetched.
309         *  @return the object contained in the record.
310         *  @throws IOException when one of the underlying I/O operations fails.
311         */
312        public Object fetch( long recid )
313            throws IOException
314        {
315            return fetch( recid, DefaultSerializer.INSTANCE );
316        }
317    
318    
319        /**
320         *  Fetches a record using a custom serializer.
321         *
322         *  @param recid the recid for the record that must be fetched.
323         *  @param serializer a custom serializer
324         *  @return the object contained in the record.
325         *  @throws IOException when one of the underlying I/O operations fails.
326         */
327        public synchronized Object fetch( long recid, Serializer serializer )
328            throws IOException
329        {
330            byte[] data;
331    
332            checkIfClosed();
333            if ( recid <= 0 ) {
334                throw new IllegalArgumentException( "Argument 'recid' is invalid: "
335                                                    + recid );
336            }
337            data = _physMgr.fetch( _logMgr.fetch( new Location( recid ) ) );
338            if ( DEBUG ) {
339                System.out.println( "BaseRecordManager.fetch() recid " + recid + " length " + data.length ) ;
340            }
341            return serializer.deserialize( data );
342        }
343    
344    
345        /**
346         *  Returns the number of slots available for "root" rowids. These slots
347         *  can be used to store special rowids, like rowids that point to
348         *  other rowids. Root rowids are useful for bootstrapping access to
349         *  a set of data.
350         */
351        public int getRootCount()
352        {
353            return FileHeader.NROOTS;
354        }
355    
356        /**
357         *  Returns the indicated root rowid.
358         *
359         *  @see #getRootCount
360         */
361        public synchronized long getRoot( int id )
362            throws IOException
363        {
364            checkIfClosed();
365    
366            return _pageman.getFileHeader().getRoot( id );
367        }
368    
369    
370        /**
371         *  Sets the indicated root rowid.
372         *
373         *  @see #getRootCount
374         */
375        public synchronized void setRoot( int id, long rowid )
376            throws IOException
377        {
378            checkIfClosed();
379    
380            _pageman.getFileHeader().setRoot( id, rowid );
381        }
382    
383    
384        /**
385         * Obtain the record id of a named object. Returns 0 if named object
386         * doesn't exist.
387         */
388        public long getNamedObject( String name )
389            throws IOException
390        {
391            checkIfClosed();
392    
393            Map nameDirectory = getNameDirectory();
394            Long recid = (Long) nameDirectory.get( name );
395            if ( recid == null ) {
396                return 0;
397            }
398            return recid.longValue();
399        }
400    
401        /**
402         * Set the record id of a named object.
403         */
404        public void setNamedObject( String name, long recid )
405            throws IOException
406        {
407            checkIfClosed();
408    
409            Map nameDirectory = getNameDirectory();
410            if ( recid == 0 ) {
411                // remove from hashtable
412                nameDirectory.remove( name );
413            } else {
414                nameDirectory.put( name, new Long( recid ) );
415            }
416            saveNameDirectory( nameDirectory );
417        }
418    
419    
420        /**
421         * Commit (make persistent) all changes since beginning of transaction.
422         */
423        public synchronized void commit()
424            throws IOException
425        {
426            checkIfClosed();
427    
428            _pageman.commit();
429        }
430    
431    
432        /**
433         * Rollback (cancel) all changes since beginning of transaction.
434         */
435        public synchronized void rollback()
436            throws IOException
437        {
438            checkIfClosed();
439    
440            _pageman.rollback();
441        }
442    
443    
444        /**
445         * Load name directory
446         */
447        private Map getNameDirectory()
448            throws IOException
449        {
450            // retrieve directory of named hashtable
451            long nameDirectory_recid = getRoot( NAME_DIRECTORY_ROOT );
452            if ( nameDirectory_recid == 0 ) {
453                _nameDirectory = new HashMap();
454                nameDirectory_recid = insert( _nameDirectory );
455                setRoot( NAME_DIRECTORY_ROOT, nameDirectory_recid );
456            } else {
457                _nameDirectory = (Map) fetch( nameDirectory_recid );
458            }
459            return _nameDirectory;
460        }
461    
462    
463        private void saveNameDirectory( Map directory )
464            throws IOException
465        {
466            long recid = getRoot( NAME_DIRECTORY_ROOT );
467            if ( recid == 0 ) {
468                throw new IOException( "Name directory must exist" );
469            }
470            update( recid, _nameDirectory );
471        }
472    
473    
474        /**
475         * Check if RecordManager has been closed.  If so, throw an
476         * IllegalStateException.
477         */
478        private void checkIfClosed()
479            throws IllegalStateException
480        {
481            if ( _file == null ) {
482                throw new IllegalStateException( "RecordManager has been closed" );
483            }
484        }
485    }