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 * +--> cn=test.ldif 084 * cn=test/ 085 * | 086 * +--> 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}