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.IOException;
26  import java.io.Writer;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.UUID;
32  
33  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
34  import org.apache.directory.api.ldap.model.csn.CsnFactory;
35  import org.apache.directory.api.ldap.model.cursor.Cursor;
36  import org.apache.directory.api.ldap.model.entry.DefaultEntry;
37  import org.apache.directory.api.ldap.model.entry.Entry;
38  import org.apache.directory.api.ldap.model.entry.Modification;
39  import org.apache.directory.api.ldap.model.exception.LdapException;
40  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
41  import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
42  import org.apache.directory.api.ldap.model.exception.LdapOperationException;
43  import org.apache.directory.api.ldap.model.exception.LdapOtherException;
44  import org.apache.directory.api.ldap.model.ldif.LdifEntry;
45  import org.apache.directory.api.ldap.model.ldif.LdifReader;
46  import org.apache.directory.api.ldap.model.ldif.LdifUtils;
47  import org.apache.directory.api.ldap.model.name.Ava;
48  import org.apache.directory.api.ldap.model.name.Dn;
49  import org.apache.directory.api.ldap.model.name.Rdn;
50  import org.apache.directory.api.ldap.model.schema.AttributeType;
51  import org.apache.directory.api.ldap.model.schema.SchemaManager;
52  import org.apache.directory.api.util.Strings;
53  import org.apache.directory.server.core.api.DnFactory;
54  import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
55  import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
56  import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
57  import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
58  import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
59  import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
60  import org.apache.directory.server.core.api.partition.PartitionTxn;
61  import org.apache.directory.server.i18n.I18n;
62  import org.apache.directory.server.xdbm.IndexEntry;
63  import org.apache.directory.server.xdbm.ParentIdAndRdn;
64  import org.apache.directory.server.xdbm.SingletonIndexCursor;
65  import org.apache.directory.server.xdbm.search.cursor.DescendantCursor;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  
70  /**
71   * A LDIF based partition. Data are stored on disk as LDIF, following this organization :
72   * <ul>
73   *   <li> each entry is associated with a file, post-fixed with LDIF
74   *   <li> each entry having at least one child will have a directory created using its name.
75   * </ul>
76   * The root is the partition's suffix.
77   * <br>
78   * So for instance, we may have on disk :
79   * <pre>
80   * /ou=example,ou=system.ldif
81   * /ou=example,ou=system/
82   *   |
83   *   +--&gt; cn=test.ldif
84   *        cn=test/
85   *           |
86   *           +--&gt; cn=another test.ldif
87   *                ...
88   * </pre>
89   * <br><br>
90   * In this exemple, the partition's suffix is <b>ou=example,ou=system</b>.
91   * <br>
92   *
93   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
94   */
95  public class LdifPartition extends AbstractLdifPartition
96  {
97      /** A logger for this class */
98      private static final Logger LOG = LoggerFactory.getLogger( LdifPartition.class );
99  
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                         LookupOperationContextceptor/context/LookupOperationContext.html#LookupOperationContext">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/interceptor/context/AddOperationContext.html#AddOperationContext">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/xdbm/IndexEntry.html#IndexEntry">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/interceptor/context/AddOperationContext.html#AddOperationContext">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 }