001/*
002 *   Licensed to the Apache Software Foundation (ASF) under one
003 *   or more contributor license agreements.  See the NOTICE file
004 *   distributed with this work for additional information
005 *   regarding copyright ownership.  The ASF licenses this file
006 *   to you under the Apache License, Version 2.0 (the
007 *   "License"); you may not use this file except in compliance
008 *   with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *   Unless required by applicable law or agreed to in writing,
013 *   software distributed under the License is distributed on an
014 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *   KIND, either express or implied.  See the License for the
016 *   specific language governing permissions and limitations
017 *   under the License.
018 *
019 */
020
021package org.apache.directory.server.core.partition.ldif;
022
023
024import java.io.File;
025import java.io.FileNotFoundException;
026import java.io.IOException;
027import java.io.RandomAccessFile;
028import java.util.Iterator;
029import java.util.UUID;
030
031import org.apache.directory.api.ldap.model.constants.SchemaConstants;
032import org.apache.directory.api.ldap.model.cursor.Cursor;
033import org.apache.directory.api.ldap.model.entry.DefaultEntry;
034import org.apache.directory.api.ldap.model.entry.Entry;
035import org.apache.directory.api.ldap.model.entry.Modification;
036import org.apache.directory.api.ldap.model.exception.LdapException;
037import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
038import org.apache.directory.api.ldap.model.exception.LdapOperationException;
039import org.apache.directory.api.ldap.model.exception.LdapOtherException;
040import org.apache.directory.api.ldap.model.ldif.LdifEntry;
041import org.apache.directory.api.ldap.model.ldif.LdifReader;
042import org.apache.directory.api.ldap.model.ldif.LdifUtils;
043import org.apache.directory.api.ldap.model.name.Dn;
044import org.apache.directory.api.ldap.model.name.Rdn;
045import org.apache.directory.api.ldap.model.schema.SchemaManager;
046import org.apache.directory.api.util.Strings;
047import org.apache.directory.server.core.api.DnFactory;
048import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
049import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
050import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
051import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
052import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
053import org.apache.directory.server.core.api.partition.PartitionTxn;
054import org.apache.directory.server.i18n.I18n;
055import org.apache.directory.server.xdbm.IndexEntry;
056import org.apache.directory.server.xdbm.ParentIdAndRdn;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060
061/**
062 * A Partition implementation backed by a single LDIF file.
063 *
064 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
065 */
066public class SingleFileLdifPartition extends AbstractLdifPartition
067{
068    /** the LDIF file holding the partition's data */
069    private RandomAccessFile ldifFile;
070
071    /** flag to enable/disable re-writing in-memory partition data back to file, default is set to true */
072    private volatile boolean enableRewriting = true;
073
074    /** flag used internally to detect if partition data was updated in memory but not on disk */
075    private boolean dirty = false;
076
077    /** lock for serializing the operations on the backing LDIF file */
078    private Object lock = new Object();
079
080    private static final Logger LOG = LoggerFactory.getLogger( SingleFileLdifPartition.class );
081
082
083    /**
084     * Creates a new instance of SingleFileLdifPartition.
085     * 
086     * @param schemaManager The SchemaManager instance
087     * @param dnFactory The DN factory
088     */
089    public SingleFileLdifPartition( SchemaManager schemaManager, DnFactory dnFactory )
090    {
091        super( schemaManager, dnFactory );
092    }
093
094
095    @Override
096    protected void doInit() throws LdapException
097    {
098        if ( !initialized )
099        {
100            if ( getPartitionPath() == null )
101            {
102                throw new IllegalArgumentException( "Partition path cannot be null" );
103            }
104
105            File partitionFile = new File( getPartitionPath() );
106
107            if ( partitionFile.exists() && !partitionFile.isFile() )
108            {
109                throw new IllegalArgumentException( "Partition path must be a LDIF file" );
110            }
111
112            try
113            {
114                ldifFile = new RandomAccessFile( partitionFile, "rws" );
115            }
116            catch ( FileNotFoundException fnfe )
117            {
118                throw new LdapOtherException( fnfe.getMessage(), fnfe );
119            }
120            
121            LOG.debug( "id is : {}", getId() );
122
123            // Initialize the suffixDirectory : it's a composition
124            // of the workingDirectory followed by the suffix
125            if ( ( suffixDn == null ) || ( suffixDn.isEmpty() ) )
126            {
127                String msg = I18n.err( I18n.ERR_150 );
128                LOG.error( msg );
129                throw new LdapInvalidDnException( msg );
130            }
131
132            if ( !suffixDn.isSchemaAware() )
133            {
134                suffixDn = new Dn( schemaManager, suffixDn );
135            }
136
137            super.doInit();
138
139            loadEntries();
140        }
141    }
142
143
144    /**
145     * load the entries from the LDIF file if present
146     * @throws Exception
147     */
148    private void loadEntries() throws LdapException
149    {
150        try ( RandomAccessLdifReader parser = new RandomAccessLdifReader( schemaManager ) )
151        {
152            Iterator<LdifEntry> itr = parser.iterator();
153    
154            if ( !itr.hasNext() )
155            {
156                return;
157            }
158    
159            LdifEntry ldifEntry = itr.next();
160    
161            contextEntry = new DefaultEntry( schemaManager, ldifEntry.getEntry() );
162    
163            if ( suffixDn.equals( contextEntry.getDn() ) )
164            {
165                addMandatoryOpAt( contextEntry );
166    
167                AddOperationContext addContext = new AddOperationContext( null, contextEntry );
168                addContext.setPartition( this );
169                addContext.setTransaction( this.beginWriteTransaction() );
170
171                super.add( addContext );
172            }
173            else
174            {
175                throw new LdapException( "The given LDIF file doesn't contain the context entry" );
176            }
177    
178            while ( itr.hasNext() )
179            {
180                ldifEntry = itr.next();
181    
182                Entry entry = new DefaultEntry( schemaManager, ldifEntry.getEntry() );
183    
184                addMandatoryOpAt( entry );
185    
186                AddOperationContext addContext = new AddOperationContext( null, entry );
187                addContext.setPartition( this );
188                addContext.setTransaction( this.beginWriteTransaction() );
189
190                super.add( addContext );
191            }
192        }
193        catch ( IOException ioe )
194        {
195            throw new LdapOtherException( ioe.getMessage(), ioe );
196        }
197    }
198
199
200    //---------------------------------------------------------------------------------------------
201    // Operations
202    //---------------------------------------------------------------------------------------------
203    /**
204     * {@inheritDoc}
205     */
206    @Override
207    public void add( AddOperationContext addContext ) throws LdapException
208    {
209        synchronized ( lock )
210        {
211            super.add( addContext );
212
213            if ( contextEntry == null )
214            {
215                Entry entry = addContext.getEntry();
216
217                if ( entry.getDn().equals( suffixDn ) )
218                {
219                    contextEntry = entry;
220                }
221            }
222
223            dirty = true;
224            rewritePartitionData( addContext.getTransaction() );
225        }
226    }
227
228
229    /**
230     * {@inheritDoc}
231     */
232    @Override
233    public void modify( ModifyOperationContext modifyContext ) throws LdapException
234    {
235        PartitionTxn partitionTxn = modifyContext.getTransaction();
236        
237        synchronized ( lock )
238        {
239            try
240            {
241                Entry modifiedEntry = super.modify( partitionTxn, modifyContext.getDn(),
242                    modifyContext.getModItems().toArray( new Modification[]
243                        {} ) );
244
245                // Remove the EntryDN
246                modifiedEntry.removeAttributes( entryDnAT );
247
248                modifyContext.setAlteredEntry( modifiedEntry );
249            }
250            catch ( Exception e )
251            {
252                throw new LdapOperationException( e.getMessage(), e );
253            }
254
255            dirty = true;
256            rewritePartitionData( partitionTxn );
257        }
258    }
259
260
261    /**
262     * {@inheritDoc}
263     */
264    @Override
265    public void rename( RenameOperationContext renameContext ) throws LdapException
266    {
267        synchronized ( lock )
268        {
269            super.rename( renameContext );
270            dirty = true;
271            rewritePartitionData( renameContext.getTransaction() );
272        }
273    }
274
275
276    /**
277     * {@inheritDoc}
278     */
279    @Override
280    public void move( MoveOperationContext moveContext ) throws LdapException
281    {
282        synchronized ( lock )
283        {
284            super.move( moveContext );
285            dirty = true;
286            rewritePartitionData( moveContext.getTransaction() );
287        }
288    }
289
290
291    /**
292     * {@inheritDoc}
293     */
294    @Override
295    public void moveAndRename( MoveAndRenameOperationContext opContext ) throws LdapException
296    {
297        synchronized ( lock )
298        {
299            super.moveAndRename( opContext );
300            dirty = true;
301            rewritePartitionData( opContext.getTransaction() );
302        }
303    }
304
305
306    @Override
307    public Entry delete( PartitionTxn partitionTxn, String id ) throws LdapException
308    {
309        synchronized ( lock )
310        {
311            Entry deletedEntry = super.delete( partitionTxn, id );
312            dirty = true;
313            rewritePartitionData( partitionTxn );
314
315            return deletedEntry;
316        }
317    }
318
319
320    /**
321     * writes the partition's data to the file if {@link #enableRewriting} is set to true
322     * and partition was modified since the last write or {@link #dirty} data. 
323     * 
324     * @throws LdapException
325     */
326    private void rewritePartitionData( PartitionTxn partitionTxn ) throws LdapException
327    {
328        synchronized ( lock )
329        {
330            if ( !enableRewriting || !dirty )
331            {
332                return;
333            }
334
335            try
336            {
337                ldifFile.setLength( 0 ); // wipe the file clean
338
339                String suffixId = getEntryId( partitionTxn, suffixDn );
340
341                if ( suffixId == null )
342                {
343                    contextEntry = null;
344                    return;
345                }
346
347                ParentIdAndRdn suffixEntry = rdnIdx.reverseLookup( partitionTxn, suffixId );
348
349                if ( suffixEntry != null )
350                {
351                    Entry entry = master.get( partitionTxn, suffixId );
352
353                    // Don't write the EntryDN attribute
354                    entry.removeAttributes( entryDnAT );
355
356                    entry.setDn( suffixDn );
357
358                    appendLdif( entry );
359
360                    appendRecursive( partitionTxn, suffixId, suffixEntry.getNbChildren() );
361                }
362
363                dirty = false;
364            }
365            catch ( LdapException e )
366            {
367                throw e;
368            }
369            catch ( Exception e )
370            {
371                throw new LdapException( e );
372            }
373        }
374    }
375
376
377    private void appendRecursive( PartitionTxn partitionTxn, String id, int nbSibbling ) throws Exception
378    {
379        // Start with the root
380        Cursor<IndexEntry<ParentIdAndRdn, String>> cursor = rdnIdx.forwardCursor( partitionTxn );
381
382        IndexEntry<ParentIdAndRdn, String> startingPos = new IndexEntry<>();
383        startingPos.setKey( new ParentIdAndRdn( id, ( Rdn[] ) null ) );
384        cursor.before( startingPos );
385        int countChildren = 0;
386
387        while ( cursor.next() && ( countChildren < nbSibbling ) )
388        {
389            IndexEntry<ParentIdAndRdn, String> element = cursor.get();
390            String childId = element.getId();
391            Entry entry = fetch( partitionTxn, childId );
392
393            // Remove the EntryDn
394            entry.removeAttributes( SchemaConstants.ENTRY_DN_AT );
395
396            appendLdif( entry );
397
398            countChildren++;
399
400            // And now, the children
401            int nbChildren = element.getKey().getNbChildren();
402
403            if ( nbChildren > 0 )
404            {
405                appendRecursive( partitionTxn, childId, nbChildren );
406            }
407        }
408
409        cursor.close();
410    }
411
412
413    /**
414     * append data to the LDIF file
415     *
416     * @param entry the entry to be written
417     * @throws LdapException
418     */
419    private void appendLdif( Entry entry ) throws IOException
420    {
421        synchronized ( lock )
422        {
423            String ldif = LdifUtils.convertToLdif( entry );
424            ldifFile.write( Strings.getBytesUtf8( ldif + "\n" ) );
425        }
426    }
427
428    /**
429     * an LdifReader backed by a RandomAccessFile
430     */
431    private class RandomAccessLdifReader extends LdifReader
432    {
433        private long len;
434
435
436        RandomAccessLdifReader() throws LdapException
437        {
438            try
439            {
440                len = ldifFile.length();
441                super.init();
442            }
443            catch ( IOException e )
444            {
445                LdapException le = new LdapException( e.getMessage(), e );
446                le.initCause( e );
447
448                throw le;
449            }
450        }
451
452
453        RandomAccessLdifReader( SchemaManager schemaManager ) throws LdapException
454        {
455            try
456            {
457                this.schemaManager = schemaManager;
458                len = ldifFile.length();
459                super.init();
460            }
461            catch ( IOException e )
462            {
463                LdapException le = new LdapException( e.getMessage(), e );
464                le.initCause( e );
465
466                throw le;
467            }
468        }
469
470
471        @Override
472        protected String getLine() throws IOException
473        {
474            if ( len == 0 )
475            {
476                return null;
477            }
478
479            return ldifFile.readLine();
480        }
481    }
482
483
484    /**
485     * add the CSN and UUID attributes to the entry if they are not present
486     */
487    private void addMandatoryOpAt( Entry entry ) throws LdapException
488    {
489        if ( entry.get( SchemaConstants.ENTRY_CSN_AT ) == null )
490        {
491            entry.add( SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString() );
492        }
493
494        if ( entry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
495        {
496            String uuid = UUID.randomUUID().toString();
497            entry.add( SchemaConstants.ENTRY_UUID_AT, uuid );
498        }
499    }
500
501
502    /**
503     * {@inheritDoc}
504     */
505    @Override
506    protected void doDestroy( PartitionTxn partitionTxn ) throws LdapException
507    {
508        super.doDestroy( partitionTxn );
509        
510        try
511        {
512            ldifFile.close();
513        }
514        catch ( IOException ioe )
515        {
516            throw new LdapOtherException( ioe.getMessage(), ioe );
517        }
518    }
519
520
521    /**
522     * enable/disable the re-writing of partition data.
523     * This method internally calls the rewritePartitionData() method to save any dirty data if present
524     * 
525     * @param partitionTxn The transaction to use
526     * @param enableRewriting flag to enable/disable re-writing
527     * @throws LdapException If we weren't able to save the dirty data
528     */
529    public void setEnableRewriting( PartitionTxn partitionTxn, boolean enableRewriting ) throws LdapException
530    {
531        this.enableRewriting = enableRewriting;
532
533        // save data if found dirty 
534        rewritePartitionData( partitionTxn );
535    }
536}