View Javadoc

1   /*
2    *   Licensed to the Apache Software Foundation (ASF) under one
3    *   or more contributor license agreements.  See the NOTICE file
4    *   distributed with this work for additional information
5    *   regarding copyright ownership.  The ASF licenses this file
6    *   to you under the Apache License, Version 2.0 (the
7    *   "License"); you may not use this file except in compliance
8    *   with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *   Unless required by applicable law or agreed to in writing,
13   *   software distributed under the License is distributed on an
14   *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *   KIND, either express or implied.  See the License for the
16   *   specific language governing permissions and limitations
17   *   under the License.
18   *
19   */
20  package org.apache.directory.server.core.partition.ldif;
21  
22  
23  import java.io.File;
24  import java.io.FileFilter;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.UUID;
30  
31  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
32  import org.apache.directory.api.ldap.model.csn.CsnFactory;
33  import org.apache.directory.api.ldap.model.cursor.Cursor;
34  import org.apache.directory.api.ldap.model.entry.DefaultEntry;
35  import org.apache.directory.api.ldap.model.entry.Entry;
36  import org.apache.directory.api.ldap.model.entry.Modification;
37  import org.apache.directory.api.ldap.model.exception.LdapException;
38  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
39  import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
40  import org.apache.directory.api.ldap.model.exception.LdapOperationException;
41  import org.apache.directory.api.ldap.model.ldif.LdifEntry;
42  import org.apache.directory.api.ldap.model.ldif.LdifReader;
43  import org.apache.directory.api.ldap.model.ldif.LdifUtils;
44  import org.apache.directory.api.ldap.model.name.Ava;
45  import org.apache.directory.api.ldap.model.name.Dn;
46  import org.apache.directory.api.ldap.model.name.Rdn;
47  import org.apache.directory.api.ldap.model.schema.AttributeType;
48  import org.apache.directory.api.ldap.model.schema.SchemaManager;
49  import org.apache.directory.api.util.Strings;
50  import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
51  import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
52  import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
53  import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
54  import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
55  import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
56  import org.apache.directory.server.i18n.I18n;
57  import org.apache.directory.server.xdbm.IndexEntry;
58  import org.apache.directory.server.xdbm.ParentIdAndRdn;
59  import org.apache.directory.server.xdbm.SingletonIndexCursor;
60  import org.apache.directory.server.xdbm.search.cursor.DescendantCursor;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  
65  /**
66   * A LDIF based partition. Data are stored on disk as LDIF, following this organization :
67   * <li> each entry is associated with a file, post-fixed with LDIF
68   * <li> each entry having at least one child will have a directory created using its name.
69   * The root is the partition's suffix.
70   * <br>
71   * So for instance, we may have on disk :
72   * <pre>
73   * /ou=example,ou=system.ldif
74   * /ou=example,ou=system/
75   *   |
76   *   +--> cn=test.ldif
77   *        cn=test/
78   *           |
79   *           +--> cn=another test.ldif
80   *                ...
81   * </pre>
82   * <br><br>
83   * In this exemple, the partition's suffix is <b>ou=example,ou=system</b>.
84   * <br>
85   *
86   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
87   */
88  public class LdifPartition extends AbstractLdifPartition
89  {
90      /** A logger for this class */
91      private static Logger LOG = LoggerFactory.getLogger( LdifPartition.class );
92  
93      /** The directory into which the entries are stored */
94      private File suffixDirectory;
95  
96      /** Flags used for the getFile() method */
97      private static final boolean CREATE = Boolean.TRUE;
98      private static final boolean DELETE = Boolean.FALSE;
99  
100     /** A filter used to pick all the directories */
101     private FileFilter dirFilter = new FileFilter()
102     {
103         public boolean accept( File dir )
104         {
105             return dir.isDirectory();
106         }
107     };
108 
109     /** A filter used to pick all the ldif entries */
110     private FileFilter entryFilter = new FileFilter()
111     {
112         public boolean accept( File dir )
113         {
114             if ( dir.getName().endsWith( CONF_FILE_EXTN ) )
115             {
116                 return dir.isFile();
117             }
118             else
119             {
120                 return false;
121             }
122         }
123     };
124 
125 
126     /**
127      * Creates a new instance of LdifPartition.
128      */
129     public LdifPartition( SchemaManager schemaManager )
130     {
131         super( schemaManager );
132     }
133 
134 
135     /**
136      * {@inheritDoc}
137      */
138     protected void doInit() throws Exception
139     {
140         if ( !initialized )
141         {
142             File partitionDir = new File( getPartitionPath() );
143 
144             // Initialize the suffixDirectory : it's a composition
145             // of the workingDirectory followed by the suffix
146             if ( ( suffixDn == null ) || ( suffixDn.isEmpty() ) )
147             {
148                 String msg = I18n.err( I18n.ERR_150 );
149                 LOG.error( msg );
150                 throw new LdapInvalidDnException( msg );
151             }
152 
153             suffixDn.apply( schemaManager );
154 
155             String suffixDirName = getFileName( suffixDn );
156             suffixDirectory = new File( partitionDir, suffixDirName );
157 
158             super.doInit();
159 
160             // Create the context entry now, if it does not exists, or load the
161             // existing entries
162             if ( suffixDirectory.exists() )
163             {
164                 loadEntries( partitionDir );
165             }
166             else
167             {
168                 // The partition directory does not exist, we have to create it, including parent directories
169                 try
170                 {
171                     suffixDirectory.mkdirs();
172                 }
173                 catch ( SecurityException se )
174                 {
175                     String msg = I18n.err( I18n.ERR_151, suffixDirectory.getAbsolutePath(), se.getLocalizedMessage() );
176                     LOG.error( msg );
177                     throw se;
178                 }
179 
180                 // And create the context entry too
181                 File contextEntryFile = new File( suffixDirectory + CONF_FILE_EXTN );
182 
183                 LOG.info( "ldif file doesn't exist {}, creating it.", contextEntryFile.getAbsolutePath() );
184 
185                 if ( contextEntry == null )
186                 {
187                     if ( contextEntryFile.exists() )
188                     {
189                         LdifReader reader = new LdifReader( contextEntryFile );
190                         contextEntry = new DefaultEntry( schemaManager, reader.next().getEntry() );
191                         reader.close();
192                     }
193                     else
194                     {
195                         // No context entry and no LDIF file exists.
196                         // Skip initialization of context entry here, it will be added later.
197                         return;
198                     }
199                 }
200 
201                 // Initialization of the context entry
202                 if ( ( suffixDn != null ) && ( contextEntry != null ) )
203                 {
204                     Dn contextEntryDn = contextEntry.getDn();
205 
206                     // Checking if the context entry DN is schema aware
207                     if ( !contextEntryDn.isSchemaAware() )
208                     {
209                         contextEntryDn.apply( schemaManager );
210                     }
211 
212                     // We're only adding the entry if the two DNs are equal
213                     if ( suffixDn.equals( contextEntryDn ) )
214                     {
215                         // Looking for the current context entry
216                         Entry suffixEntry = lookup( new LookupOperationContext( null, suffixDn ) );
217 
218                         // We're only adding the context entry if it doesn't already exist
219                         if ( suffixEntry == null )
220                         {
221                             // Checking of the context entry is schema aware
222                             if ( !contextEntry.isSchemaAware() )
223                             {
224                                 // Making the context entry schema aware
225                                 contextEntry = new DefaultEntry( schemaManager, contextEntry );
226                             }
227 
228                             // Adding the 'entryCsn' attribute
229                             if ( contextEntry.get( SchemaConstants.ENTRY_CSN_AT ) == null )
230                             {
231                                 contextEntry.add( SchemaConstants.ENTRY_CSN_AT, new CsnFactory( 0 ).newInstance()
232                                     .toString() );
233                             }
234 
235                             // Adding the 'entryUuid' attribute
236                             if ( contextEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
237                             {
238                                 String uuid = UUID.randomUUID().toString();
239                                 contextEntry.add( SchemaConstants.ENTRY_UUID_AT, uuid );
240                             }
241 
242                             // And add this entry to the underlying partition
243                             add( new AddOperationContext( null, contextEntry ) );
244                         }
245                     }
246                 }
247             }
248         }
249     }
250 
251 
252     //-------------------------------------------------------------------------
253     // Operations
254     //-------------------------------------------------------------------------
255     /**
256      * {@inheritDoc}
257      */
258     public void add( AddOperationContext addContext ) throws LdapException
259     {
260         super.add( addContext );
261         
262         addEntry( addContext.getEntry() );
263     }
264 
265 
266     /**
267      * {@inheritDoc}
268      */
269     public Entry delete( String id ) throws LdapException
270     {
271         Entry deletedEntry = super.delete( id );
272 
273         if ( deletedEntry != null )
274         {
275             File ldifFile = getFile( deletedEntry.getDn(), DELETE );
276 
277             boolean deleted = deleteFile( ldifFile );
278 
279             LOG.debug( "deleted file {} {}", ldifFile.getAbsoluteFile(), deleted );
280 
281             // Delete the parent if there is no more children
282             File parentFile = ldifFile.getParentFile();
283 
284             if ( parentFile.listFiles().length == 0 )
285             {
286                 deleteFile( parentFile );
287 
288                 LOG.debug( "deleted file {} {}", parentFile.getAbsoluteFile(), deleted );
289             }
290         }
291         
292         return deletedEntry;
293     }
294 
295 
296     /**
297      * {@inheritDoc}
298      */
299     public void modify( ModifyOperationContext modifyContext ) throws LdapException
300     {
301         String id = getEntryId( modifyContext.getDn() );
302 
303         try
304         {
305             super.modify( modifyContext.getDn(), modifyContext.getModItems().toArray( new Modification[]
306                 {} ) );
307         }
308         catch ( Exception e )
309         {
310             throw new LdapOperationException( e.getMessage(), e );
311         }
312 
313         // Get the modified entry and store it in the context for post usage
314         Entry modifiedEntry = fetch( id, modifyContext.getDn() );
315         modifyContext.setAlteredEntry( modifiedEntry );
316 
317         // Remove the EntryDN
318         modifiedEntry.removeAttributes( ENTRY_DN_AT );
319 
320         // just overwrite the existing file
321         Dn dn = modifyContext.getDn();
322 
323         // And write it back on disk
324         try
325         {
326             FileWriter fw = new FileWriter( getFile( dn, DELETE ) );
327             fw.write( LdifUtils.convertToLdif( modifiedEntry, true ) );
328             fw.close();
329         }
330         catch ( IOException ioe )
331         {
332             throw new LdapOperationException( ioe.getMessage(), ioe );
333         }
334     }
335 
336 
337     /**
338      * {@inheritDoc}
339      */
340     public void move( MoveOperationContext moveContext ) throws LdapException
341     {
342         Dn oldDn = moveContext.getDn();
343         String id = getEntryId( oldDn );
344 
345         super.move( moveContext );
346 
347         // Get the modified entry
348         Entry modifiedEntry = fetch( id, moveContext.getNewDn() );
349 
350         try
351         {
352             entryMoved( oldDn, modifiedEntry, id );
353         }
354         catch ( Exception e )
355         {
356             throw new LdapOperationErrorException( e.getMessage(), e );
357         }
358     }
359 
360 
361     /**
362      * {@inheritDoc}
363      */
364     public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
365     {
366         Dn oldDn = moveAndRenameContext.getDn();
367         String id = getEntryId( oldDn );
368 
369         super.moveAndRename( moveAndRenameContext );
370 
371         // Get the modified entry and store it in the context for post usage
372         Entry modifiedEntry = fetch( id, moveAndRenameContext.getNewDn() );
373         moveAndRenameContext.setModifiedEntry( modifiedEntry );
374 
375         try
376         {
377             entryMoved( oldDn, modifiedEntry, id );
378         }
379         catch ( Exception e )
380         {
381             throw new LdapOperationErrorException( e.getMessage(), e );
382         }
383     }
384 
385 
386     /**
387      * {@inheritDoc}
388      */
389     public void rename( RenameOperationContext renameContext ) throws LdapException
390     {
391         Dn oldDn = renameContext.getDn();
392         String id = getEntryId( oldDn );
393 
394         // Create the new entry
395         super.rename( renameContext );
396 
397         // Get the modified entry and store it in the context for post usage
398         Dn newDn = oldDn.getParent().add( renameContext.getNewRdn() );
399         Entry modifiedEntry = fetch( id, newDn );
400         renameContext.setModifiedEntry( modifiedEntry );
401 
402         // Now move the potential children for the old entry
403         // and remove the old entry
404         try
405         {
406             entryMoved( oldDn, modifiedEntry, id );
407         }
408         catch ( Exception e )
409         {
410             throw new LdapOperationErrorException( e.getMessage(), e );
411         }
412     }
413 
414 
415     /**
416      * rewrites the moved entry and its associated children
417      * Note that instead of moving and updating the existing files on disk
418      * this method gets the moved entry and its children and writes the LDIF files
419      *
420      * @param oldEntryDn the moved entry's old Dn
421      * @param entryId the moved entry's master table ID
422      * @param deleteOldEntry a flag to tell whether to delete the old entry files
423      * @throws Exception
424      */
425     private void entryMoved( Dn oldEntryDn, Entry modifiedEntry, String entryIdOld ) throws Exception
426     {
427         // First, add the new entry
428         addEntry( modifiedEntry );
429 
430         String baseId = getEntryId( modifiedEntry.getDn() );
431 
432         ParentIdAndRdn parentIdAndRdn = getRdnIndex().reverseLookup( baseId );
433         IndexEntry indexEntry = new IndexEntry();
434 
435         indexEntry.setId( baseId );
436         indexEntry.setKey( parentIdAndRdn );
437 
438         Cursor<IndexEntry<ParentIdAndRdn, String>> cursor = new SingletonIndexCursor<ParentIdAndRdn>(
439             indexEntry );
440         String parentId = parentIdAndRdn.getParentId();
441 
442         Cursor<IndexEntry<String, String>> scopeCursor = new DescendantCursor( this, baseId, parentId, cursor );
443 
444         // Then, if there are some children, move then to the new place
445         try
446         {
447             while ( scopeCursor.next() )
448             {
449                 IndexEntry<String, String> entry = scopeCursor.get();
450 
451                 // except the parent entry add the rest of entries
452                 if ( entry.getId() != entryIdOld )
453                 {
454                     addEntry( fetch( entry.getId() ) );
455                 }
456             }
457 
458             scopeCursor.close();
459         }
460         catch ( Exception e )
461         {
462             throw new LdapOperationException( e.getMessage(), e );
463         }
464 
465         // And delete the old entry's LDIF file
466         File file = getFile( oldEntryDn, DELETE );
467         boolean deleted = deleteFile( file );
468         LOG.warn( "move operation: deleted file {} {}", file.getAbsoluteFile(), deleted );
469 
470         // and the associated directory ( the file's name's minus ".ldif")
471         String dirName = file.getAbsolutePath();
472         dirName = dirName.substring( 0, dirName.indexOf( CONF_FILE_EXTN ) );
473         deleted = deleteFile( new File( dirName ) );
474         LOG.warn( "move operation: deleted dir {} {}", dirName, deleted );
475     }
476 
477 
478     /**
479      * loads the configuration into the DIT from the file system
480      * Note that it assumes the presence of a directory with the partition suffix's upname
481      * under the partition's base dir
482      *
483      * for ex. if 'config' is the partition's id and 'ou=config' is its suffix it looks for the dir with the path
484      *
485      * <directory-service-working-dir>/config/ou=config
486      * e.x example.com/config/ou=config
487      *
488      * NOTE: this dir setup is just to ease the testing of this partition, this needs to be
489      * replaced with some kind of bootstrapping the default config from a jar file and
490      * write to the FS in LDIF format
491      *
492      * @throws Exception
493      */
494     private void loadEntries( File entryDir ) throws Exception
495     {
496         LOG.debug( "Processing dir {}", entryDir.getName() );
497 
498         // First, load the entries
499         File[] entries = entryDir.listFiles( entryFilter );
500 
501         if ( ( entries != null ) && ( entries.length != 0 ) )
502         {
503             LdifReader ldifReader = new LdifReader();
504 
505             for ( File entry : entries )
506             {
507                 LOG.debug( "parsing ldif file {}", entry.getName() );
508                 List<LdifEntry> ldifEntries = ldifReader.parseLdifFile( entry.getAbsolutePath() );
509                 ldifReader.close();
510 
511                 if ( ( ldifEntries != null ) && !ldifEntries.isEmpty() )
512                 {
513                     // this ldif will have only one entry
514                     LdifEntry ldifEntry = ldifEntries.get( 0 );
515                     LOG.debug( "Adding entry {}", ldifEntry );
516 
517                     Entry serverEntry = new DefaultEntry( schemaManager, ldifEntry.getEntry() );
518 
519                     if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_CSN_AT ) )
520                     {
521                         serverEntry.put( SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString() );
522                     }
523 
524                     if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_UUID_AT ) )
525                     {
526                         serverEntry.put( SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString() );
527                     }
528 
529                     // call add on the wrapped partition not on the self
530                     AddOperationContext addContext = new AddOperationContext( null, serverEntry );
531                     super.add( addContext );
532                 }
533             }
534 
535         }
536         else
537         {
538             // If we don't have ldif files, we won't have sub-directories
539             return;
540         }
541 
542         // Second, recurse on the sub directories
543         File[] dirs = entryDir.listFiles( dirFilter );
544 
545         if ( ( dirs != null ) && ( dirs.length != 0 ) )
546         {
547             for ( File f : dirs )
548             {
549                 loadEntries( f );
550             }
551         }
552     }
553 
554 
555     /**
556      * Create the file name from the entry Dn.
557      */
558     private File getFile( Dn entryDn, boolean create ) throws LdapException
559     {
560         String parentDir = null;
561         String rdnFileName = null;
562 
563         if ( entryDn.equals( suffixDn ) )
564         {
565             parentDir = suffixDirectory.getParent() + File.separator;
566             rdnFileName = suffixDn.getName() + CONF_FILE_EXTN;
567         }
568         else
569         {
570             StringBuilder filePath = new StringBuilder();
571             filePath.append( suffixDirectory ).append( File.separator );
572 
573             Dn baseDn = entryDn.getDescendantOf( suffixDn );
574             int size = baseDn.size();
575 
576             for ( int i = 0; i < size - 1; i++ )
577             {
578                 rdnFileName = getFileName( baseDn.getRdn( size - 1 - i ) );
579 
580                 filePath.append( rdnFileName ).append( File.separator );
581             }
582 
583             rdnFileName = getFileName( entryDn.getRdn() ) + CONF_FILE_EXTN;
584             parentDir = filePath.toString();
585         }
586 
587         File dir = new File( parentDir );
588 
589         if ( !dir.exists() && create )
590         {
591             // We have to create the entry if it does not have a parent
592             if ( !dir.mkdir() )
593             {
594                 throw new LdapException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECORY, dir ) );
595             }
596         }
597 
598         File ldifFile = new File( parentDir + rdnFileName );
599 
600         if ( ldifFile.exists() && create )
601         {
602             // The entry already exists
603             throw new LdapException( I18n.err( I18n.ERR_633 ) );
604         }
605 
606         return ldifFile;
607     }
608 
609 
610     /**
611      * Compute the real name based on the Rdn, assuming that depending on the underlying
612      * OS, some characters are not allowed.
613      *
614      * We don't allow filename which length is > 255 chars.
615      */
616     private String getFileName( Rdn rdn ) throws LdapException
617     {
618         StringBuilder fileName = new StringBuilder( "" );
619 
620         Iterator<Ava> iterator = rdn.iterator();
621 
622         while ( iterator.hasNext() )
623         {
624             Ava ava = iterator.next();
625 
626             // First, get the AT name, or OID
627             String normAT = ava.getNormType();
628             AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
629 
630             String atName = at.getName();
631 
632             // Now, get the normalized value
633             String normValue = ava.getNormValue().getString();
634 
635             fileName.append( atName ).append( "=" ).append( normValue );
636 
637             if ( iterator.hasNext() )
638             {
639                 fileName.append( "+" );
640             }
641         }
642 
643         return getOSFileName( fileName.toString() );
644     }
645 
646 
647     /**
648      * Compute the real name based on the Dn, assuming that depending on the underlying
649      * OS, some characters are not allowed.
650      *
651      * We don't allow filename which length is > 255 chars.
652      */
653     private String getFileName( Dn dn ) throws LdapException
654     {
655         StringBuilder sb = new StringBuilder();
656         boolean isFirst = true;
657 
658         for ( Rdn rdn : dn.getRdns() )
659         {
660             // First, get the AT name, or OID
661             String normAT = rdn.getNormType();
662             AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
663 
664             String atName = at.getName();
665 
666             // Now, get the normalized value
667             String normValue = rdn.getNormValue().getString();
668 
669             if ( isFirst )
670             {
671                 isFirst = false;
672             }
673             else
674             {
675                 sb.append( "," );
676             }
677 
678             sb.append( atName ).append( "=" ).append( normValue );
679         }
680 
681         return getOSFileName( sb.toString() );
682     }
683 
684 
685     /**
686      * Get a OS compatible file name. We URL encode all characters that may cause trouble
687      * according to http://en.wikipedia.org/wiki/Filenames. This includes C0 control characters
688      * [0x00-0x1F] and 0x7F, see http://en.wikipedia.org/wiki/Control_characters.
689      */
690     private String getOSFileName( String fileName )
691     {
692         StringBuilder sb = new StringBuilder();
693 
694         for ( char c : fileName.toCharArray() )
695         {
696             switch ( c )
697             {
698                 case 0x00:
699                 case 0x01:
700                 case 0x02:
701                 case 0x03:
702                 case 0x04:
703                 case 0x05:
704                 case 0x06:
705                 case 0x07:
706                 case 0x08:
707                 case 0x09:
708                 case 0x0A:
709                 case 0x0B:
710                 case 0x0C:
711                 case 0x0D:
712                 case 0x0E:
713                 case 0x0F:
714                 case 0x10:
715                 case 0x11:
716                 case 0x12:
717                 case 0x13:
718                 case 0x14:
719                 case 0x15:
720                 case 0x16:
721                 case 0x17:
722                 case 0x18:
723                 case 0x19:
724                 case 0x1A:
725                 case 0x1B:
726                 case 0x1C:
727                 case 0x1D:
728                 case 0x1E:
729                 case 0x1F:
730                 case 0x7F:
731                 case ' ': // 0x20
732                 case '"': // 0x22
733                 case '%': // 0x25
734                 case '&': // 0x26
735                 case '(': // 0x28
736                 case ')': // 0x29
737                 case '*': // 0x2A
738                 case '+': // 0x2B
739                 case '/': // 0x2F
740                 case ':': // 0x3A
741                 case ';': // 0x3B
742                 case '<': // 0x3C
743                 case '>': // 0x3E
744                 case '?': // 0x3F
745                 case '[': // 0x5B
746                 case '\\': // 0x5C
747                 case ']': // 0x5D
748                 case '|': // 0x7C
749                     sb.append( "%" ).append( Strings.dumpHex( ( byte ) ( c >> 4 ) ) )
750                         .append( Strings.dumpHex( ( byte ) ( c & 0xF ) ) );
751                     break;
752 
753                 default:
754                     sb.append( c );
755                     break;
756             }
757         }
758 
759         return Strings.toLowerCase( sb.toString() );
760     }
761 
762 
763     /**
764      * Write the new entry on disk. It does not exist, as this has been checked
765      * by the ExceptionInterceptor.
766      */
767     private void addEntry( Entry entry ) throws LdapException
768     {
769         // Remove the EntryDN
770         entry.removeAttributes( ENTRY_DN_AT );
771 
772         try
773         {
774             FileWriter fw = new FileWriter( getFile( entry.getDn(), CREATE ) );
775             fw.write( LdifUtils.convertToLdif( entry ) );
776             fw.close();
777         }
778         catch ( IOException ioe )
779         {
780             throw new LdapOperationException( ioe.getMessage(), ioe );
781         }
782     }
783 
784 
785     /**
786      * Recursively delete an entry and all of its children. If the entry is a directory,
787      * then get into it, call the same method on each of the contained files,
788      * and delete the directory.
789      */
790     private boolean deleteFile( File file )
791     {
792         if ( file.isDirectory() )
793         {
794             File[] files = file.listFiles();
795 
796             // Process the contained files
797             for ( File f : files )
798             {
799                 deleteFile( f );
800             }
801 
802             // then delete the directory itself
803             return file.delete();
804         }
805         else
806         {
807             return file.delete();
808         }
809     }
810 }