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 */
020package org.apache.directory.server.core.partition.ldif;
021
022
023import java.io.File;
024import java.io.FileFilter;
025import java.io.IOException;
026import java.io.Writer;
027import java.nio.charset.StandardCharsets;
028import java.nio.file.Files;
029import java.util.Iterator;
030import java.util.List;
031import java.util.UUID;
032
033import org.apache.directory.api.ldap.model.constants.SchemaConstants;
034import org.apache.directory.api.ldap.model.csn.CsnFactory;
035import org.apache.directory.api.ldap.model.cursor.Cursor;
036import org.apache.directory.api.ldap.model.entry.DefaultEntry;
037import org.apache.directory.api.ldap.model.entry.Entry;
038import org.apache.directory.api.ldap.model.entry.Modification;
039import org.apache.directory.api.ldap.model.exception.LdapException;
040import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
041import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
042import org.apache.directory.api.ldap.model.exception.LdapOperationException;
043import org.apache.directory.api.ldap.model.exception.LdapOtherException;
044import org.apache.directory.api.ldap.model.ldif.LdifEntry;
045import org.apache.directory.api.ldap.model.ldif.LdifReader;
046import org.apache.directory.api.ldap.model.ldif.LdifUtils;
047import org.apache.directory.api.ldap.model.name.Ava;
048import org.apache.directory.api.ldap.model.name.Dn;
049import org.apache.directory.api.ldap.model.name.Rdn;
050import org.apache.directory.api.ldap.model.schema.AttributeType;
051import org.apache.directory.api.ldap.model.schema.SchemaManager;
052import org.apache.directory.api.util.Strings;
053import org.apache.directory.server.core.api.DnFactory;
054import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
055import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
056import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
057import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
058import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
059import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
060import org.apache.directory.server.core.api.partition.PartitionTxn;
061import org.apache.directory.server.i18n.I18n;
062import org.apache.directory.server.xdbm.IndexEntry;
063import org.apache.directory.server.xdbm.ParentIdAndRdn;
064import org.apache.directory.server.xdbm.SingletonIndexCursor;
065import org.apache.directory.server.xdbm.search.cursor.DescendantCursor;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069
070/**
071 * A LDIF based partition. Data are stored on disk as LDIF, following this organization :
072 * <ul>
073 *   <li> each entry is associated with a file, post-fixed with LDIF
074 *   <li> each entry having at least one child will have a directory created using its name.
075 * </ul>
076 * The root is the partition's suffix.
077 * <br>
078 * So for instance, we may have on disk :
079 * <pre>
080 * /ou=example,ou=system.ldif
081 * /ou=example,ou=system/
082 *   |
083 *   +--&gt; cn=test.ldif
084 *        cn=test/
085 *           |
086 *           +--&gt; cn=another test.ldif
087 *                ...
088 * </pre>
089 * <br><br>
090 * In this exemple, the partition's suffix is <b>ou=example,ou=system</b>.
091 * <br>
092 *
093 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
094 */
095public class LdifPartition extends AbstractLdifPartition
096{
097    /** A logger for this class */
098    private static final Logger LOG = LoggerFactory.getLogger( LdifPartition.class );
099
100    /** The directory into which the entries are stored */
101    private File suffixDirectory;
102
103    /** Flags used for the getFile() method */
104    private static final boolean CREATE = Boolean.TRUE;
105    private static final boolean DELETE = Boolean.FALSE;
106
107    /** A filter used to pick all the directories */
108    private FileFilter dirFilter = new FileFilter()
109    {
110        public boolean accept( File dir )
111        {
112            return dir.isDirectory();
113        }
114    };
115
116    /** A filter used to pick all the ldif entries */
117    private FileFilter entryFilter = new FileFilter()
118    {
119        public boolean accept( File dir )
120        {
121            if ( dir.getName().endsWith( CONF_FILE_EXTN ) )
122            {
123                return dir.isFile();
124            }
125            else
126            {
127                return false;
128            }
129        }
130    };
131
132
133    /**
134     * Creates a new instance of LdifPartition.
135     * 
136     * @param schemaManager The SchemaManager instance
137     * @param dnFactory The DN factory
138     */
139    public LdifPartition( SchemaManager schemaManager, DnFactory dnFactory )
140    {
141        super( schemaManager, dnFactory );
142    }
143
144
145    /**
146     * {@inheritDoc}
147     */
148    @Override
149    protected void doInit() throws LdapException
150    {
151        if ( !initialized )
152        {
153            File partitionDir = new File( getPartitionPath() );
154
155            // Initialize the suffixDirectory : it's a composition
156            // of the workingDirectory followed by the suffix
157            if ( ( suffixDn == null ) || ( suffixDn.isEmpty() ) )
158            {
159                String msg = I18n.err( I18n.ERR_150 );
160                LOG.error( msg );
161                throw new LdapInvalidDnException( msg );
162            }
163
164            if ( !suffixDn.isSchemaAware() )
165            {
166                suffixDn = new Dn( schemaManager, suffixDn );
167            }
168
169            String suffixDirName = getFileName( suffixDn );
170            suffixDirectory = new File( partitionDir, suffixDirName );
171
172            super.doInit();
173
174            // Create the context entry now, if it does not exists, or load the
175            // existing entries
176            if ( suffixDirectory.exists() )
177            {
178                loadEntries( partitionDir );
179            }
180            else
181            {
182                // The partition directory does not exist, we have to create it, including parent directories
183                try
184                {
185                    suffixDirectory.mkdirs();
186                }
187                catch ( SecurityException se )
188                {
189                    String msg = I18n.err( I18n.ERR_151, suffixDirectory.getAbsolutePath(), se.getLocalizedMessage() );
190                    LOG.error( msg );
191                    throw se;
192                }
193
194                // And create the context entry too
195                File contextEntryFile = new File( suffixDirectory + CONF_FILE_EXTN );
196
197                LOG.info( "ldif file doesn't exist {}, creating it.", contextEntryFile.getAbsolutePath() );
198
199                if ( contextEntry == null )
200                {
201                    if ( contextEntryFile.exists() )
202                    {
203                        try ( LdifReader reader = new LdifReader( contextEntryFile ) )
204                        {
205                            contextEntry = new DefaultEntry( schemaManager, reader.next().getEntry() );
206                        }
207                        catch ( IOException ioe )
208                        {
209                            throw new LdapOtherException( ioe.getMessage(), ioe );
210                        }
211                    }
212                    else
213                    {
214                        // No context entry and no LDIF file exists.
215                        // Skip initialization of context entry here, it will be added later.
216                        return;
217                    }
218                }
219
220                // Initialization of the context entry
221                if ( suffixDn != null )
222                {
223                    Dn contextEntryDn = contextEntry.getDn();
224
225                    // Checking if the context entry DN is schema aware
226                    if ( !contextEntryDn.isSchemaAware() )
227                    {
228                        contextEntryDn = new Dn( schemaManager, contextEntryDn );
229                    }
230
231                    // We're only adding the entry if the two DNs are equal
232                    if ( suffixDn.equals( contextEntryDn ) )
233                    {
234                        // Looking for the current context entry
235                        Entry suffixEntry;
236                        
237                        LookupOperationContext lookupContext = new LookupOperationContext( null, suffixDn );
238                        lookupContext.setPartition( this );
239
240                        try ( PartitionTxn partitionTxn = this.beginReadTransaction() )
241                        {
242                            lookupContext.setTransaction( partitionTxn );
243                            suffixEntry = lookup( lookupContext );
244                        }
245                        catch ( IOException ioe )
246                        {
247                            throw new LdapOtherException( ioe.getMessage(), ioe );
248                        }
249
250                        // We're only adding the context entry if it doesn't already exist
251                        if ( suffixEntry == null )
252                        {
253                            // Checking of the context entry is schema aware
254                            if ( !contextEntry.isSchemaAware() )
255                            {
256                                // Making the context entry schema aware
257                                contextEntry = new DefaultEntry( schemaManager, contextEntry );
258                            }
259
260                            // Adding the 'entryCsn' attribute
261                            if ( contextEntry.get( SchemaConstants.ENTRY_CSN_AT ) == null )
262                            {
263                                contextEntry.add( SchemaConstants.ENTRY_CSN_AT, new CsnFactory( 0 ).newInstance()
264                                    .toString() );
265                            }
266
267                            // Adding the 'entryUuid' attribute
268                            if ( contextEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
269                            {
270                                String uuid = UUID.randomUUID().toString();
271                                contextEntry.add( SchemaConstants.ENTRY_UUID_AT, uuid );
272                            }
273
274                            // And add this entry to the underlying partition
275                            AddOperationContext addContext = new AddOperationContext( null, contextEntry );
276                            addContext.setPartition( this );
277                            PartitionTxn partitionTxn = null;
278                            
279                            try
280                            {
281                                partitionTxn = beginWriteTransaction();
282                                addContext.setTransaction( partitionTxn );
283                            
284                                add( addContext );
285                                partitionTxn.commit();
286                            }
287                            catch ( Exception e )
288                            {
289                                try
290                                {
291                                    if ( partitionTxn != null )
292                                    {
293                                        partitionTxn.abort();
294                                    }
295                                }
296                                catch ( IOException ioe )
297                                {
298                                    throw new LdapOtherException( ioe.getMessage(), ioe );
299                                }
300                            }
301                        }
302                    }
303                }
304            }
305        }
306    }
307
308
309    //-------------------------------------------------------------------------
310    // Operations
311    //-------------------------------------------------------------------------
312    /**
313     * {@inheritDoc}
314     */
315    @Override
316    public void add( AddOperationContext addContext ) throws LdapException
317    {
318        super.add( addContext );
319
320        addEntry( addContext.getEntry() );
321    }
322
323
324    /**
325     * {@inheritDoc}
326     */
327    @Override
328    public Entry delete( PartitionTxn partitionTxn, String id ) throws LdapException
329    {
330        Entry deletedEntry = super.delete( partitionTxn, id );
331
332        if ( deletedEntry != null )
333        {
334            File ldifFile = getFile( deletedEntry.getDn(), DELETE );
335
336            boolean deleted = deleteFile( ldifFile );
337
338            LOG.debug( "deleted file {} {}", ldifFile.getAbsoluteFile(), deleted );
339
340            // Delete the parent if there is no more children
341            File parentFile = ldifFile.getParentFile();
342
343            if ( parentFile.listFiles().length == 0 )
344            {
345                deleteFile( parentFile );
346
347                LOG.debug( "deleted file {} {}", parentFile.getAbsoluteFile(), deleted );
348            }
349        }
350
351        return deletedEntry;
352    }
353
354
355    /**
356     * {@inheritDoc}
357     */
358    @Override
359    public void modify( ModifyOperationContext modifyContext ) throws LdapException
360    {
361        PartitionTxn partitionTxn = modifyContext.getTransaction();
362        String id = getEntryId( partitionTxn, modifyContext.getDn() );
363
364        try
365        {
366            super.modify( modifyContext.getTransaction(), modifyContext.getDn(), modifyContext.getModItems().toArray( new Modification[]
367                {} ) );
368        }
369        catch ( Exception e )
370        {
371            throw new LdapOperationException( e.getMessage(), e );
372        }
373
374        // Get the modified entry and store it in the context for post usage
375        Entry modifiedEntry = fetch( modifyContext.getTransaction(), id, modifyContext.getDn() );
376        modifyContext.setAlteredEntry( modifiedEntry );
377
378        // Remove the EntryDN
379        modifiedEntry.removeAttributes( entryDnAT );
380
381        // just overwrite the existing file
382        Dn dn = modifyContext.getDn();
383
384        // And write it back on disk
385        
386        try ( Writer fw = Files.newBufferedWriter( getFile( dn, DELETE ).toPath(), StandardCharsets.UTF_8 ) )
387        {
388            fw.write( LdifUtils.convertToLdif( modifiedEntry, true ) );
389        }
390        catch ( IOException ioe )
391        {
392            throw new LdapOperationException( ioe.getMessage(), ioe );
393        }
394    }
395
396
397    /**
398     * {@inheritDoc}
399     */
400    @Override
401    public void move( MoveOperationContext moveContext ) throws LdapException
402    {
403        PartitionTxn partitionTxn = moveContext.getTransaction();
404        Dn oldDn = moveContext.getDn();
405        String id = getEntryId( partitionTxn, oldDn );
406
407        super.move( moveContext );
408
409        // Get the modified entry
410        Entry modifiedEntry = fetch( moveContext.getTransaction(), id, moveContext.getNewDn() );
411
412        try
413        {
414            entryMoved( partitionTxn, oldDn, modifiedEntry, id );
415        }
416        catch ( Exception e )
417        {
418            throw new LdapOperationErrorException( e.getMessage(), e );
419        }
420    }
421
422
423    /**
424     * {@inheritDoc}
425     */
426    @Override
427    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
428    {
429        PartitionTxn partitionTxn = moveAndRenameContext.getTransaction(); 
430        Dn oldDn = moveAndRenameContext.getDn();
431        String id = getEntryId( partitionTxn, oldDn );
432
433        super.moveAndRename( moveAndRenameContext );
434
435        // Get the modified entry and store it in the context for post usage
436        Entry modifiedEntry = fetch( moveAndRenameContext.getTransaction(), id, moveAndRenameContext.getNewDn() );
437        moveAndRenameContext.setModifiedEntry( modifiedEntry );
438
439        try
440        {
441            entryMoved( partitionTxn, oldDn, modifiedEntry, id );
442        }
443        catch ( Exception e )
444        {
445            throw new LdapOperationErrorException( e.getMessage(), e );
446        }
447    }
448
449
450    /**
451     * {@inheritDoc}
452     */
453    @Override
454    public void rename( RenameOperationContext renameContext ) throws LdapException
455    {
456        PartitionTxn partitionTxn = renameContext.getTransaction(); 
457        Dn oldDn = renameContext.getDn();
458        String entryId = getEntryId( partitionTxn, oldDn );
459
460        // Create the new entry
461        super.rename( renameContext );
462
463        // Get the modified entry and store it in the context for post usage
464        Dn newDn = oldDn.getParent().add( renameContext.getNewRdn() );
465        Entry modifiedEntry = fetch( renameContext.getTransaction(), entryId, newDn );
466        renameContext.setModifiedEntry( modifiedEntry );
467
468        // Now move the potential children for the old entry
469        // and remove the old entry
470        try
471        {
472            entryMoved( partitionTxn, oldDn, modifiedEntry, entryId );
473        }
474        catch ( Exception e )
475        {
476            throw new LdapOperationErrorException( e.getMessage(), e );
477        }
478    }
479
480
481    /**
482     * rewrites the moved entry and its associated children
483     * Note that instead of moving and updating the existing files on disk
484     * this method gets the moved entry and its children and writes the LDIF files
485     *
486     * @param oldEntryDn the moved entry's old Dn
487     * @param entryId the moved entry's master table ID
488     * @param deleteOldEntry a flag to tell whether to delete the old entry files
489     * @throws Exception
490     */
491    private void entryMoved( PartitionTxn partitionTxn, Dn oldEntryDn, Entry modifiedEntry, String entryIdOld ) throws LdapException
492    {
493        // First, add the new entry
494        addEntry( modifiedEntry );
495
496        String baseId = getEntryId( partitionTxn, modifiedEntry.getDn() );
497
498        ParentIdAndRdn parentIdAndRdn = getRdnIndex().reverseLookup( partitionTxn, baseId );
499        IndexEntry indexEntry = new IndexEntry();
500
501        indexEntry.setId( baseId );
502        indexEntry.setKey( parentIdAndRdn );
503
504        Cursor<IndexEntry<ParentIdAndRdn, String>> cursor = new SingletonIndexCursor<>( partitionTxn, indexEntry );
505        String parentId = parentIdAndRdn.getParentId();
506
507        Cursor<IndexEntry<String, String>> scopeCursor = new DescendantCursor( partitionTxn, this, baseId, parentId, cursor );
508
509        // Then, if there are some children, move then to the new place
510        try
511        {
512            while ( scopeCursor.next() )
513            {
514                IndexEntry<String, String> entry = scopeCursor.get();
515
516                // except the parent entry add the rest of entries
517                if ( entry.getId() != entryIdOld )
518                {
519                    addEntry( fetch( partitionTxn, entry.getId() ) );
520                }
521            }
522
523            scopeCursor.close();
524        }
525        catch ( Exception e )
526        {
527            throw new LdapOperationException( e.getMessage(), e );
528        }
529
530        // And delete the old entry's LDIF file
531        File file = getFile( oldEntryDn, DELETE );
532        boolean deleted = deleteFile( file );
533        LOG.warn( "move operation: deleted file {} {}", file.getAbsoluteFile(), deleted );
534
535        // and the associated directory ( the file's name's minus ".ldif")
536        String dirName = file.getAbsolutePath();
537        dirName = dirName.substring( 0, dirName.indexOf( CONF_FILE_EXTN ) );
538        deleted = deleteFile( new File( dirName ) );
539        LOG.warn( "move operation: deleted dir {} {}", dirName, deleted );
540    }
541
542
543    /**
544     * loads the configuration into the DIT from the file system
545     * Note that it assumes the presence of a directory with the partition suffix's upname
546     * under the partition's base dir
547     *
548     * for ex. if 'config' is the partition's id and 'ou=config' is its suffix it looks for the dir with the path
549     *
550     * <directory-service-working-dir>/config/ou=config
551     * e.x example.com/config/ou=config
552     *
553     * NOTE: this dir setup is just to ease the testing of this partition, this needs to be
554     * replaced with some kind of bootstrapping the default config from a jar file and
555     * write to the FS in LDIF format
556     *
557     * @throws Exception
558     */
559    private void loadEntries( File entryDir ) throws LdapException
560    {
561        LOG.debug( "Processing dir {}", entryDir.getName() );
562
563        // First, load the entries
564        File[] entries = entryDir.listFiles( entryFilter );
565
566        if ( ( entries != null ) && ( entries.length != 0 ) )
567        {
568            LdifReader ldifReader = new LdifReader( schemaManager );
569
570            for ( File entry : entries )
571            {
572                LOG.debug( "parsing ldif file {}", entry.getName() );
573                List<LdifEntry> ldifEntries = ldifReader.parseLdifFile( entry.getAbsolutePath() );
574                
575                try
576                {
577                    ldifReader.close();
578                }
579                catch ( IOException ioe )
580                {
581                    throw new LdapOtherException( ioe.getMessage(), ioe );
582                }
583
584                if ( ( ldifEntries != null ) && !ldifEntries.isEmpty() )
585                {
586                    // this ldif will have only one entry
587                    LdifEntry ldifEntry = ldifEntries.get( 0 );
588                    LOG.debug( "Adding entry {}", ldifEntry );
589
590                    Entry serverEntry = new DefaultEntry( schemaManager, ldifEntry.getEntry() );
591
592                    if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_CSN_AT ) )
593                    {
594                        serverEntry.put( SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString() );
595                    }
596
597                    if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_UUID_AT ) )
598                    {
599                        serverEntry.put( SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString() );
600                    }
601
602                    // call add on the wrapped partition not on the self
603                    AddOperationContext addContext = new AddOperationContext( null, serverEntry );
604                    PartitionTxn partitionTxn = beginWriteTransaction();
605                    
606                    try
607                    {
608                        addContext.setTransaction( partitionTxn );
609                        addContext.setPartition( this );
610                    
611                        super.add( addContext );
612                        
613                        partitionTxn.commit();
614                    }
615                    catch ( LdapException le )
616                    {
617                        try
618                        {
619                            partitionTxn.abort();
620                        }
621                        catch ( IOException ioe )
622                        {
623                            throw new LdapOtherException( ioe.getMessage(), ioe );
624                        }
625                        
626                        throw le;
627                    }
628                    catch ( IOException ioe )
629                    {
630                        try
631                        {
632                            partitionTxn.abort();
633                        }
634                        catch ( IOException ioe2 )
635                        {
636                            throw new LdapOtherException( ioe2.getMessage(), ioe2 );
637                        }
638                        
639                        throw new LdapOtherException( ioe.getMessage(), ioe );
640                    }
641                }
642            }
643
644        }
645        else
646        {
647            // If we don't have ldif files, we won't have sub-directories
648            return;
649        }
650
651        // Second, recurse on the sub directories
652        File[] dirs = entryDir.listFiles( dirFilter );
653
654        if ( ( dirs != null ) && ( dirs.length != 0 ) )
655        {
656            for ( File f : dirs )
657            {
658                loadEntries( f );
659            }
660        }
661    }
662
663
664    /**
665     * Create the file name from the entry Dn.
666     */
667    private File getFile( Dn entryDn, boolean create ) throws LdapException
668    {
669        String parentDir = null;
670        String rdnFileName = null;
671
672        if ( entryDn.equals( suffixDn ) )
673        {
674            parentDir = suffixDirectory.getParent() + File.separator;
675            rdnFileName = suffixDn.getName() + CONF_FILE_EXTN;
676        }
677        else
678        {
679            StringBuilder filePath = new StringBuilder();
680            filePath.append( suffixDirectory ).append( File.separator );
681
682            Dn baseDn = entryDn.getDescendantOf( suffixDn );
683            int size = baseDn.size();
684
685            for ( int i = 0; i < size - 1; i++ )
686            {
687                rdnFileName = getFileName( baseDn.getRdn( size - 1 - i ) );
688
689                filePath.append( rdnFileName ).append( File.separator );
690            }
691
692            rdnFileName = getFileName( entryDn.getRdn() ) + CONF_FILE_EXTN;
693            parentDir = filePath.toString();
694        }
695
696        File dir = new File( parentDir );
697
698        if ( !dir.exists() && create )
699        {
700            // We have to create the entry if it does not have a parent
701            if ( !dir.mkdir() )
702            {
703                throw new LdapException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, dir ) );
704            }
705        }
706
707        File ldifFile = new File( parentDir + rdnFileName );
708
709        if ( ldifFile.exists() && create )
710        {
711            // The entry already exists
712            throw new LdapException( I18n.err( I18n.ERR_633 ) );
713        }
714
715        return ldifFile;
716    }
717
718
719    /**
720     * Compute the real name based on the Rdn, assuming that depending on the underlying
721     * OS, some characters are not allowed.
722     *
723     * We don't allow filename which length is > 255 chars.
724     */
725    private String getFileName( Rdn rdn ) throws LdapException
726    {
727        StringBuilder fileName = new StringBuilder( "" );
728
729        Iterator<Ava> iterator = rdn.iterator();
730
731        while ( iterator.hasNext() )
732        {
733            Ava ava = iterator.next();
734
735            // First, get the AT name, or OID
736            String normAT = ava.getNormType();
737            AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
738
739            String atName = at.getName();
740
741            // Now, get the normalized value
742            String normValue = null;
743            
744            if ( at.getSyntax().isHumanReadable() )
745            {
746                normValue = ava.getValue().getString();
747            }
748            else
749            {
750                normValue = Strings.utf8ToString( ava.getValue().getBytes() );
751            }
752
753            fileName.append( atName ).append( "=" ).append( normValue );
754
755            if ( iterator.hasNext() )
756            {
757                fileName.append( "+" );
758            }
759        }
760
761        return getOSFileName( fileName.toString() );
762    }
763
764
765    /**
766     * Compute the real name based on the Dn, assuming that depending on the underlying
767     * OS, some characters are not allowed.
768     *
769     * We don't allow filename which length is > 255 chars.
770     */
771    private String getFileName( Dn dn ) throws LdapException
772    {
773        StringBuilder sb = new StringBuilder();
774        boolean isFirst = true;
775
776        for ( Rdn rdn : dn.getRdns() )
777        {
778            // First, get the AT name, or OID
779            String normAT = rdn.getNormType();
780            AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
781
782            String atName = at.getName();
783
784            // Now, get the normalized value
785            String normValue = rdn.getAva().getValue().getString();
786
787            if ( isFirst )
788            {
789                isFirst = false;
790            }
791            else
792            {
793                sb.append( "," );
794            }
795
796            sb.append( atName ).append( "=" ).append( normValue );
797        }
798
799        return getOSFileName( sb.toString() );
800    }
801
802
803    /**
804     * Get a OS compatible file name. We URL encode all characters that may cause trouble
805     * according to http://en.wikipedia.org/wiki/Filenames. This includes C0 control characters
806     * [0x00-0x1F] and 0x7F, see http://en.wikipedia.org/wiki/Control_characters.
807     */
808    private String getOSFileName( String fileName )
809    {
810        StringBuilder sb = new StringBuilder();
811
812        for ( char c : fileName.toCharArray() )
813        {
814            switch ( c )
815            {
816                case 0x00:
817                case 0x01:
818                case 0x02:
819                case 0x03:
820                case 0x04:
821                case 0x05:
822                case 0x06:
823                case 0x07:
824                case 0x08:
825                case 0x09:
826                case 0x0A:
827                case 0x0B:
828                case 0x0C:
829                case 0x0D:
830                case 0x0E:
831                case 0x0F:
832                case 0x10:
833                case 0x11:
834                case 0x12:
835                case 0x13:
836                case 0x14:
837                case 0x15:
838                case 0x16:
839                case 0x17:
840                case 0x18:
841                case 0x19:
842                case 0x1A:
843                case 0x1B:
844                case 0x1C:
845                case 0x1D:
846                case 0x1E:
847                case 0x1F:
848                case 0x7F:
849                case ' ': // 0x20
850                case '"': // 0x22
851                case '%': // 0x25
852                case '&': // 0x26
853                case '(': // 0x28
854                case ')': // 0x29
855                case '*': // 0x2A
856                case '+': // 0x2B
857                case '/': // 0x2F
858                case ':': // 0x3A
859                case ';': // 0x3B
860                case '<': // 0x3C
861                case '>': // 0x3E
862                case '?': // 0x3F
863                case '[': // 0x5B
864                case '\\': // 0x5C
865                case ']': // 0x5D
866                case '|': // 0x7C
867                    sb.append( "%" ).append( Strings.dumpHex( ( byte ) ( c >> 4 ) ) )
868                        .append( Strings.dumpHex( ( byte ) ( c & 0xF ) ) );
869                    break;
870
871                default:
872                    sb.append( c );
873                    break;
874            }
875        }
876
877        return Strings.toLowerCaseAscii( sb.toString() );
878    }
879
880
881    /**
882     * Write the new entry on disk. It does not exist, as this has been checked
883     * by the ExceptionInterceptor.
884     */
885    private void addEntry( Entry entry ) throws LdapException
886    {
887        // Remove the EntryDN
888        entry.removeAttributes( entryDnAT );
889
890        try ( Writer fw = Files.newBufferedWriter( getFile( entry.getDn(), CREATE ).toPath(), StandardCharsets.UTF_8 ) )
891        {
892            fw.write( LdifUtils.convertToLdif( entry ) );
893        }
894        catch ( IOException ioe )
895        {
896            throw new LdapOperationException( ioe.getMessage(), ioe );
897        }
898    }
899
900
901    /**
902     * Recursively delete an entry and all of its children. If the entry is a directory,
903     * then get into it, call the same method on each of the contained files,
904     * and delete the directory.
905     */
906    private boolean deleteFile( File file )
907    {
908        if ( file.isDirectory() )
909        {
910            File[] files = file.listFiles();
911
912            // Process the contained files
913            for ( File f : files )
914            {
915                deleteFile( f );
916            }
917
918            // then delete the directory itself
919            return file.delete();
920        }
921        else
922        {
923            return file.delete();
924        }
925    }
926}