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 org.apache.directory.server.i18n.I18n;
057    
058    import jdbm.RecordManager;
059    import jdbm.helper.Serializer;
060    import jdbm.helper.DefaultSerializer;
061    
062    /**
063     *  This class manages records, which are uninterpreted blobs of data. The
064     *  set of operations is simple and straightforward: you communicate with
065     *  the class using long "rowids" and byte[] data blocks. Rowids are returned
066     *  on inserts and you can stash them away someplace safe to be able to get
067     *  back to them. Data blocks can be as long as you wish, and may have
068     *  lengths different from the original when updating.
069     *  <p>
070     *  Operations are synchronized, so that only one of them will happen
071     *  concurrently even if you hammer away from multiple threads. Operations
072     *  are made atomic by keeping a transaction log which is recovered after
073     *  a crash, so the operations specified by this interface all have ACID
074     *  properties.
075     *  <p>
076     *  You identify a file by just the name. The package attaches <tt>.db</tt>
077     *  for the database file, and <tt>.lg</tt> for the transaction log. The
078     *  transaction log is synchronized regularly and then restarted, so don't
079     *  worry if you see the size going up and down.
080     *
081     * @author <a href="mailto:boisvert@intalio.com">Alex Boisvert</a>
082     * @author <a href="cg@cdegroot.com">Cees de Groot</a>
083     * @version $Id: BaseRecordManager.java,v 1.8 2005/06/25 23:12:32 doomdark Exp $
084     */
085    public final class BaseRecordManager
086        implements RecordManager
087    {
088    
089        /** Underlying record file. */
090        private RecordFile recordFile;
091    
092        /** Physical row identifier manager. */
093        private PhysicalRowIdManager physMgr;
094    
095        /** Logical to Physical row identifier manager. */
096        private LogicalRowIdManager logMgr;
097    
098        /** Page manager. */
099        private PageManager pageMgr;
100    
101        /** Reserved slot for name directory. */
102        public static final int NAME_DIRECTORY_ROOT = 0;
103    
104        /** Static debugging flag */
105        public static final boolean DEBUG = false;
106    
107        /**
108         * Directory of named JDBMHashtables.  This directory is a persistent
109         * directory, stored as a Hashtable.  It can be retrieved by using
110         * the NAME_DIRECTORY_ROOT.
111         */
112        private Map<String,Long> nameDirectory;
113    
114    
115        /**
116         * Creates a record manager for the indicated file
117         *
118         * @throws IOException when the file cannot be opened or is not
119         *         a valid file content-wise.
120         */
121        public BaseRecordManager( String filename ) throws IOException
122        {
123            recordFile = new RecordFile( filename );
124            pageMgr = new PageManager( recordFile );
125            physMgr = new PhysicalRowIdManager( recordFile, pageMgr );
126            logMgr = new LogicalRowIdManager( recordFile, pageMgr );
127        }
128    
129    
130        /**
131         * Get the underlying Transaction Manager
132         */
133        public synchronized TransactionManager getTransactionManager()
134        {
135            checkIfClosed();
136            return recordFile.txnMgr;
137        }
138    
139    
140        /**
141         * Switches off transactions for the record manager. This means
142         * that a) a transaction log is not kept, and b) writes aren't
143         * synch'ed after every update. This is useful when batch inserting
144         * into a new database.
145         *  <p>
146         *  Only call this method directly after opening the file, otherwise
147         *  the results will be undefined.
148         */
149        public synchronized void disableTransactions()
150        {
151            checkIfClosed();
152            recordFile.disableTransactions();
153        }
154    
155        
156        /**
157         * Closes the record manager.
158         *
159         * @throws IOException when one of the underlying I/O operations fails.
160         */
161        public synchronized void close() throws IOException
162        {
163            checkIfClosed();
164    
165            pageMgr.close();
166            pageMgr = null;
167    
168            recordFile.close();
169            recordFile = null;
170        }
171    
172    
173        /**
174         * Inserts a new record using standard java object serialization.
175         *
176         * @param obj the object for the new record.
177         * @return the rowid for the new record.
178         * @throws IOException when one of the underlying I/O operations fails.
179         */
180        public long insert( Object obj ) throws IOException
181        {
182            return insert( obj, DefaultSerializer.INSTANCE );
183        }
184    
185        
186        /**
187         * Inserts a new record using a custom serializer.
188         *
189         * @param obj the object for the new record.
190         * @param serializer a custom serializer
191         * @return the rowid for the new record.
192         * @throws IOException when one of the underlying I/O operations fails.
193         */
194        public synchronized long insert( Object obj, Serializer serializer ) throws IOException
195        {
196            byte[]    data;
197            long      recid;
198            Location  physRowId;
199            
200            checkIfClosed();
201    
202            data = serializer.serialize( obj );
203            physRowId = physMgr.insert( data, 0, data.length );
204            recid = logMgr.insert( physRowId ).toLong();
205         
206            if ( DEBUG ) 
207            {
208                System.out.println( "BaseRecordManager.insert() recid " + recid + " length " + data.length ) ;
209            }
210            
211            return recid;
212        }
213    
214        
215        /**
216         * Deletes a record.
217         *
218         * @param recid the rowid for the record that should be deleted.
219         * @throws IOException when one of the underlying I/O operations fails.
220         */
221        public synchronized void delete( long recid ) throws IOException
222        {
223            checkIfClosed();
224            
225            if ( recid <= 0 ) 
226            {
227                throw new IllegalArgumentException( I18n.err( I18n.ERR_536, recid ) );
228            }
229    
230            if ( DEBUG ) 
231            {
232                System.out.println( "BaseRecordManager.delete() recid " + recid ) ;
233            }
234    
235            Location logRowId = new Location( recid );
236            Location physRowId = logMgr.fetch( logRowId );
237            physMgr.delete( physRowId );
238            logMgr.delete( logRowId );
239        }
240    
241    
242        /**
243         * Updates a record using standard java object serialization.
244         *
245         * @param recid the recid for the record that is to be updated.
246         * @param obj the new object for the record.
247         * @throws IOException when one of the underlying I/O operations fails.
248         */
249        public void update( long recid, Object obj ) throws IOException
250        {
251            update( recid, obj, DefaultSerializer.INSTANCE );
252        }
253    
254        
255        /**
256         * Updates a record using a custom serializer.
257         *
258         * @param recid the recid for the record that is to be updated.
259         * @param obj the new object for the record.
260         * @param serializer a custom serializer
261         * @throws IOException when one of the underlying I/O operations fails.
262         */
263        public synchronized void update( long recid, Object obj, Serializer serializer ) throws IOException
264        {
265            checkIfClosed();
266    
267            if ( recid <= 0 ) 
268            {
269                throw new IllegalArgumentException( I18n.err( I18n.ERR_536, recid ) );
270            }
271    
272            Location logRecid = new Location( recid );
273            Location physRecid = logMgr.fetch( logRecid );
274            
275            byte[] data = serializer.serialize( obj );
276            
277            if ( DEBUG ) 
278            {
279                System.out.println( "BaseRecordManager.update() recid " + recid + " length " + data.length ) ;
280            }
281            
282            Location newRecid = physMgr.update( physRecid, data, 0, data.length );
283            
284            if ( ! newRecid.equals( physRecid ) ) 
285            {
286                logMgr.update( logRecid, newRecid );
287            }
288        }
289    
290    
291        /**
292         * Fetches a record using standard java object serialization.
293         *
294         * @param recid the recid for the record that must be fetched.
295         * @return the object contained in the record.
296         * @throws IOException when one of the underlying I/O operations fails.
297         */
298        public Object fetch( long recid ) throws IOException
299        {
300            return fetch( recid, DefaultSerializer.INSTANCE );
301        }
302    
303    
304        /**
305         * Fetches a record using a custom serializer.
306         *
307         * @param recid the recid for the record that must be fetched.
308         * @param serializer a custom serializer
309         * @return the object contained in the record.
310         * @throws IOException when one of the underlying I/O operations fails.
311         */
312        public synchronized Object fetch( long recid, Serializer serializer )
313            throws IOException
314        {
315            byte[] data;
316    
317            checkIfClosed();
318           
319            if ( recid <= 0 ) 
320            {
321                throw new IllegalArgumentException( I18n.err( I18n.ERR_536, recid ) );
322            }
323            
324            data = physMgr.fetch( logMgr.fetch( new Location( recid ) ) );
325            
326            if ( DEBUG ) 
327            {
328                System.out.println( "BaseRecordManager.fetch() recid " + recid + " length " + data.length ) ;
329            }
330            return serializer.deserialize( data );
331        }
332    
333    
334        /**
335         * Returns the number of slots available for "root" rowids. These slots
336         * can be used to store special rowids, like rowids that point to
337         * other rowids. Root rowids are useful for bootstrapping access to
338         * a set of data.
339         */
340        public int getRootCount()
341        {
342            return FileHeader.NROOTS;
343        }
344    
345        
346        /**
347         *  Returns the indicated root rowid.
348         *
349         *  @see #getRootCount
350         */
351        public synchronized long getRoot( int id ) throws IOException
352        {
353            checkIfClosed();
354    
355            return pageMgr.getFileHeader().getRoot( id );
356        }
357    
358    
359        /**
360         *  Sets the indicated root rowid.
361         *
362         *  @see #getRootCount
363         */
364        public synchronized void setRoot( int id, long rowid ) throws IOException
365        {
366            checkIfClosed();
367    
368            pageMgr.getFileHeader().setRoot( id, rowid );
369        }
370    
371    
372        /**
373         * Obtain the record id of a named object. Returns 0 if named object
374         * doesn't exist.
375         */
376        public long getNamedObject( String name ) throws IOException
377        {
378            checkIfClosed();
379    
380            Map<String,Long> nameDirectory = getNameDirectory();
381            Long recid = nameDirectory.get( name );
382    
383            if ( recid == null ) 
384            {
385                return 0;
386            }
387            
388            return recid;
389        }
390        
391    
392        /**
393         * Set the record id of a named object.
394         */
395        public void setNamedObject( String name, long recid ) throws IOException
396        {
397            checkIfClosed();
398    
399            Map<String,Long> nameDirectory = getNameDirectory();
400            if ( recid == 0 ) 
401            {
402                // remove from hashtable
403                nameDirectory.remove( name );
404            } 
405            else 
406            {
407                nameDirectory.put( name, recid );
408            }
409            saveNameDirectory( nameDirectory );
410        }
411    
412    
413        /**
414         * Commit (make persistent) all changes since beginning of transaction.
415         */
416        public synchronized void commit()
417            throws IOException
418        {
419            checkIfClosed();
420    
421            pageMgr.commit();
422        }
423    
424    
425        /**
426         * Rollback (cancel) all changes since beginning of transaction.
427         */
428        public synchronized void rollback() throws IOException
429        {
430            checkIfClosed();
431    
432            pageMgr.rollback();
433        }
434    
435    
436        /**
437         * Load name directory
438         */
439        @SuppressWarnings("unchecked")
440        private Map<String,Long> getNameDirectory() throws IOException
441        {
442            // retrieve directory of named hashtable
443            long nameDirectory_recid = getRoot( NAME_DIRECTORY_ROOT );
444            
445            if ( nameDirectory_recid == 0 ) 
446            {
447                nameDirectory = new HashMap<String, Long>();
448                nameDirectory_recid = insert( nameDirectory );
449                setRoot( NAME_DIRECTORY_ROOT, nameDirectory_recid );
450            } 
451            else 
452            {
453                nameDirectory = ( Map<String, Long> ) fetch( nameDirectory_recid );
454            }
455            
456            return nameDirectory;
457        }
458    
459    
460        private void saveNameDirectory( Map<String,Long> directory ) throws IOException
461        {
462            long recid = getRoot( NAME_DIRECTORY_ROOT );
463            
464            if ( recid == 0 ) 
465            {
466                throw new IOException( I18n.err( I18n.ERR_537 ) );
467            }
468            
469            update( recid, nameDirectory );
470        }
471    
472    
473        /**
474         * Check if RecordManager has been closed.  If so, throw an IllegalStateException.
475         */
476        private void checkIfClosed() throws IllegalStateException
477        {
478            if ( recordFile == null ) 
479            {
480                throw new IllegalStateException( I18n.err( I18n.ERR_538 ) );
481            }
482        }
483    }