blob: dbd0c36d7fcdcd244e2205e2437ffbf6a296bd7e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.directory.server.core.partition.ldif;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.csn.CsnFactory;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
import org.apache.directory.api.ldap.model.exception.LdapOperationException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.ldif.LdifUtils;
import org.apache.directory.api.ldap.model.name.Ava;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.api.partition.PartitionTxn;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.xdbm.IndexEntry;
import org.apache.directory.server.xdbm.ParentIdAndRdn;
import org.apache.directory.server.xdbm.SingletonIndexCursor;
import org.apache.directory.server.xdbm.search.cursor.DescendantCursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A LDIF based partition. Data are stored on disk as LDIF, following this organization :
* <ul>
* <li> each entry is associated with a file, post-fixed with LDIF
* <li> each entry having at least one child will have a directory created using its name.
* </ul>
* The root is the partition's suffix.
* <br>
* So for instance, we may have on disk :
* <pre>
* /ou=example,ou=system.ldif
* /ou=example,ou=system/
* |
* +--&gt; cn=test.ldif
* cn=test/
* |
* +--&gt; cn=another test.ldif
* ...
* </pre>
* <br><br>
* In this exemple, the partition's suffix is <b>ou=example,ou=system</b>.
* <br>
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class LdifPartition extends AbstractLdifPartition
{
/** A logger for this class */
private static final Logger LOG = LoggerFactory.getLogger( LdifPartition.class );
/** The directory into which the entries are stored */
private File suffixDirectory;
/** Flags used for the getFile() method */
private static final boolean CREATE = Boolean.TRUE;
private static final boolean DELETE = Boolean.FALSE;
/** A filter used to pick all the directories */
private FileFilter dirFilter = new FileFilter()
{
public boolean accept( File dir )
{
return dir.isDirectory();
}
};
/** A filter used to pick all the ldif entries */
private FileFilter entryFilter = new FileFilter()
{
public boolean accept( File dir )
{
if ( dir.getName().endsWith( CONF_FILE_EXTN ) )
{
return dir.isFile();
}
else
{
return false;
}
}
};
/**
* Creates a new instance of LdifPartition.
*
* @param schemaManager The SchemaManager instance
* @param dnFactory The DN factory
*/
public LdifPartition( SchemaManager schemaManager, DnFactory dnFactory )
{
super( schemaManager, dnFactory );
}
/**
* {@inheritDoc}
*/
@Override
protected void doInit() throws LdapException
{
if ( !initialized )
{
File partitionDir = new File( getPartitionPath() );
// Initialize the suffixDirectory : it's a composition
// of the workingDirectory followed by the suffix
if ( ( suffixDn == null ) || ( suffixDn.isEmpty() ) )
{
String msg = I18n.err( I18n.ERR_150 );
LOG.error( msg );
throw new LdapInvalidDnException( msg );
}
if ( !suffixDn.isSchemaAware() )
{
suffixDn = new Dn( schemaManager, suffixDn );
}
String suffixDirName = getFileName( suffixDn );
suffixDirectory = new File( partitionDir, suffixDirName );
super.doInit();
// Create the context entry now, if it does not exists, or load the
// existing entries
if ( suffixDirectory.exists() )
{
loadEntries( partitionDir );
}
else
{
// The partition directory does not exist, we have to create it, including parent directories
try
{
suffixDirectory.mkdirs();
}
catch ( SecurityException se )
{
String msg = I18n.err( I18n.ERR_151, suffixDirectory.getAbsolutePath(), se.getLocalizedMessage() );
LOG.error( msg );
throw se;
}
// And create the context entry too
File contextEntryFile = new File( suffixDirectory + CONF_FILE_EXTN );
LOG.info( "ldif file doesn't exist {}, creating it.", contextEntryFile.getAbsolutePath() );
if ( contextEntry == null )
{
if ( contextEntryFile.exists() )
{
try ( LdifReader reader = new LdifReader( contextEntryFile ) )
{
contextEntry = new DefaultEntry( schemaManager, reader.next().getEntry() );
}
catch ( IOException ioe )
{
throw new LdapOtherException( ioe.getMessage(), ioe );
}
}
else
{
// No context entry and no LDIF file exists.
// Skip initialization of context entry here, it will be added later.
return;
}
}
// Initialization of the context entry
if ( suffixDn != null )
{
Dn contextEntryDn = contextEntry.getDn();
// Checking if the context entry DN is schema aware
if ( !contextEntryDn.isSchemaAware() )
{
contextEntryDn = new Dn( schemaManager, contextEntryDn );
}
// We're only adding the entry if the two DNs are equal
if ( suffixDn.equals( contextEntryDn ) )
{
// Looking for the current context entry
Entry suffixEntry;
LookupOperationContext lookupContext = new LookupOperationContext( null, suffixDn );
lookupContext.setPartition( this );
try ( PartitionTxn partitionTxn = this.beginReadTransaction() )
{
lookupContext.setTransaction( partitionTxn );
suffixEntry = lookup( lookupContext );
}
catch ( IOException ioe )
{
throw new LdapOtherException( ioe.getMessage(), ioe );
}
// We're only adding the context entry if it doesn't already exist
if ( suffixEntry == null )
{
// Checking of the context entry is schema aware
if ( !contextEntry.isSchemaAware() )
{
// Making the context entry schema aware
contextEntry = new DefaultEntry( schemaManager, contextEntry );
}
// Adding the 'entryCsn' attribute
if ( contextEntry.get( SchemaConstants.ENTRY_CSN_AT ) == null )
{
contextEntry.add( SchemaConstants.ENTRY_CSN_AT, new CsnFactory( 0 ).newInstance()
.toString() );
}
// Adding the 'entryUuid' attribute
if ( contextEntry.get( SchemaConstants.ENTRY_UUID_AT ) == null )
{
String uuid = UUID.randomUUID().toString();
contextEntry.add( SchemaConstants.ENTRY_UUID_AT, uuid );
}
// And add this entry to the underlying partition
AddOperationContext addContext = new AddOperationContext( null, contextEntry );
addContext.setPartition( this );
PartitionTxn partitionTxn = null;
try
{
partitionTxn = beginWriteTransaction();
addContext.setTransaction( partitionTxn );
add( addContext );
partitionTxn.commit();
}
catch ( Exception e )
{
try
{
if ( partitionTxn != null )
{
partitionTxn.abort();
}
}
catch ( IOException ioe )
{
throw new LdapOtherException( ioe.getMessage(), ioe );
}
}
}
}
}
}
}
}
//-------------------------------------------------------------------------
// Operations
//-------------------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void add( AddOperationContext addContext ) throws LdapException
{
super.add( addContext );
addEntry( addContext.getEntry() );
}
/**
* {@inheritDoc}
*/
@Override
public Entry delete( PartitionTxn partitionTxn, String id ) throws LdapException
{
Entry deletedEntry = super.delete( partitionTxn, id );
if ( deletedEntry != null )
{
File ldifFile = getFile( deletedEntry.getDn(), DELETE );
boolean deleted = deleteFile( ldifFile );
LOG.debug( "deleted file {} {}", ldifFile.getAbsoluteFile(), deleted );
// Delete the parent if there is no more children
File parentFile = ldifFile.getParentFile();
if ( parentFile.listFiles().length == 0 )
{
deleteFile( parentFile );
LOG.debug( "deleted file {} {}", parentFile.getAbsoluteFile(), deleted );
}
}
return deletedEntry;
}
/**
* {@inheritDoc}
*/
@Override
public void modify( ModifyOperationContext modifyContext ) throws LdapException
{
PartitionTxn partitionTxn = modifyContext.getTransaction();
String id = getEntryId( partitionTxn, modifyContext.getDn() );
try
{
super.modify( modifyContext.getTransaction(), modifyContext.getDn(), modifyContext.getModItems().toArray( new Modification[]
{} ) );
}
catch ( Exception e )
{
throw new LdapOperationException( e.getMessage(), e );
}
// Get the modified entry and store it in the context for post usage
Entry modifiedEntry = fetch( modifyContext.getTransaction(), id, modifyContext.getDn() );
modifyContext.setAlteredEntry( modifiedEntry );
// Remove the EntryDN
modifiedEntry.removeAttributes( entryDnAT );
// just overwrite the existing file
Dn dn = modifyContext.getDn();
// And write it back on disk
try ( Writer fw = Files.newBufferedWriter( getFile( dn, DELETE ).toPath(), StandardCharsets.UTF_8 ) )
{
fw.write( LdifUtils.convertToLdif( modifiedEntry, true ) );
}
catch ( IOException ioe )
{
throw new LdapOperationException( ioe.getMessage(), ioe );
}
}
/**
* {@inheritDoc}
*/
@Override
public void move( MoveOperationContext moveContext ) throws LdapException
{
PartitionTxn partitionTxn = moveContext.getTransaction();
Dn oldDn = moveContext.getDn();
String id = getEntryId( partitionTxn, oldDn );
super.move( moveContext );
// Get the modified entry
Entry modifiedEntry = fetch( moveContext.getTransaction(), id, moveContext.getNewDn() );
try
{
entryMoved( partitionTxn, oldDn, modifiedEntry, id );
}
catch ( Exception e )
{
throw new LdapOperationErrorException( e.getMessage(), e );
}
}
/**
* {@inheritDoc}
*/
@Override
public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
{
PartitionTxn partitionTxn = moveAndRenameContext.getTransaction();
Dn oldDn = moveAndRenameContext.getDn();
String id = getEntryId( partitionTxn, oldDn );
super.moveAndRename( moveAndRenameContext );
// Get the modified entry and store it in the context for post usage
Entry modifiedEntry = fetch( moveAndRenameContext.getTransaction(), id, moveAndRenameContext.getNewDn() );
moveAndRenameContext.setModifiedEntry( modifiedEntry );
try
{
entryMoved( partitionTxn, oldDn, modifiedEntry, id );
}
catch ( Exception e )
{
throw new LdapOperationErrorException( e.getMessage(), e );
}
}
/**
* {@inheritDoc}
*/
@Override
public void rename( RenameOperationContext renameContext ) throws LdapException
{
PartitionTxn partitionTxn = renameContext.getTransaction();
Dn oldDn = renameContext.getDn();
String entryId = getEntryId( partitionTxn, oldDn );
// Create the new entry
super.rename( renameContext );
// Get the modified entry and store it in the context for post usage
Dn newDn = oldDn.getParent().add( renameContext.getNewRdn() );
Entry modifiedEntry = fetch( renameContext.getTransaction(), entryId, newDn );
renameContext.setModifiedEntry( modifiedEntry );
// Now move the potential children for the old entry
// and remove the old entry
try
{
entryMoved( partitionTxn, oldDn, modifiedEntry, entryId );
}
catch ( Exception e )
{
throw new LdapOperationErrorException( e.getMessage(), e );
}
}
/**
* rewrites the moved entry and its associated children
* Note that instead of moving and updating the existing files on disk
* this method gets the moved entry and its children and writes the LDIF files
*
* @param oldEntryDn the moved entry's old Dn
* @param entryId the moved entry's master table ID
* @param deleteOldEntry a flag to tell whether to delete the old entry files
* @throws Exception
*/
private void entryMoved( PartitionTxn partitionTxn, Dn oldEntryDn, Entry modifiedEntry, String entryIdOld ) throws LdapException
{
// First, add the new entry
addEntry( modifiedEntry );
String baseId = getEntryId( partitionTxn, modifiedEntry.getDn() );
ParentIdAndRdn parentIdAndRdn = getRdnIndex().reverseLookup( partitionTxn, baseId );
IndexEntry indexEntry = new IndexEntry();
indexEntry.setId( baseId );
indexEntry.setKey( parentIdAndRdn );
Cursor<IndexEntry<ParentIdAndRdn, String>> cursor = new SingletonIndexCursor<>( partitionTxn, indexEntry );
String parentId = parentIdAndRdn.getParentId();
Cursor<IndexEntry<String, String>> scopeCursor = new DescendantCursor( partitionTxn, this, baseId, parentId, cursor );
// Then, if there are some children, move then to the new place
try
{
while ( scopeCursor.next() )
{
IndexEntry<String, String> entry = scopeCursor.get();
// except the parent entry add the rest of entries
if ( entry.getId() != entryIdOld )
{
addEntry( fetch( partitionTxn, entry.getId() ) );
}
}
scopeCursor.close();
}
catch ( Exception e )
{
throw new LdapOperationException( e.getMessage(), e );
}
// And delete the old entry's LDIF file
File file = getFile( oldEntryDn, DELETE );
boolean deleted = deleteFile( file );
LOG.warn( "move operation: deleted file {} {}", file.getAbsoluteFile(), deleted );
// and the associated directory ( the file's name's minus ".ldif")
String dirName = file.getAbsolutePath();
dirName = dirName.substring( 0, dirName.indexOf( CONF_FILE_EXTN ) );
deleted = deleteFile( new File( dirName ) );
LOG.warn( "move operation: deleted dir {} {}", dirName, deleted );
}
/**
* loads the configuration into the DIT from the file system
* Note that it assumes the presence of a directory with the partition suffix's upname
* under the partition's base dir
*
* for ex. if 'config' is the partition's id and 'ou=config' is its suffix it looks for the dir with the path
*
* <directory-service-working-dir>/config/ou=config
* e.x example.com/config/ou=config
*
* NOTE: this dir setup is just to ease the testing of this partition, this needs to be
* replaced with some kind of bootstrapping the default config from a jar file and
* write to the FS in LDIF format
*
* @throws Exception
*/
private void loadEntries( File entryDir ) throws LdapException
{
LOG.debug( "Processing dir {}", entryDir.getName() );
// First, load the entries
File[] entries = entryDir.listFiles( entryFilter );
if ( ( entries != null ) && ( entries.length != 0 ) )
{
LdifReader ldifReader = new LdifReader( schemaManager );
for ( File entry : entries )
{
LOG.debug( "parsing ldif file {}", entry.getName() );
List<LdifEntry> ldifEntries = ldifReader.parseLdifFile( entry.getAbsolutePath() );
try
{
ldifReader.close();
}
catch ( IOException ioe )
{
throw new LdapOtherException( ioe.getMessage(), ioe );
}
if ( ( ldifEntries != null ) && !ldifEntries.isEmpty() )
{
// this ldif will have only one entry
LdifEntry ldifEntry = ldifEntries.get( 0 );
LOG.debug( "Adding entry {}", ldifEntry );
Entry serverEntry = new DefaultEntry( schemaManager, ldifEntry.getEntry() );
if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_CSN_AT ) )
{
serverEntry.put( SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString() );
}
if ( !serverEntry.containsAttribute( SchemaConstants.ENTRY_UUID_AT ) )
{
serverEntry.put( SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString() );
}
// call add on the wrapped partition not on the self
AddOperationContext addContext = new AddOperationContext( null, serverEntry );
PartitionTxn partitionTxn = beginWriteTransaction();
try
{
addContext.setTransaction( partitionTxn );
addContext.setPartition( this );
super.add( addContext );
partitionTxn.commit();
}
catch ( LdapException le )
{
try
{
partitionTxn.abort();
}
catch ( IOException ioe )
{
throw new LdapOtherException( ioe.getMessage(), ioe );
}
throw le;
}
catch ( IOException ioe )
{
try
{
partitionTxn.abort();
}
catch ( IOException ioe2 )
{
throw new LdapOtherException( ioe2.getMessage(), ioe2 );
}
throw new LdapOtherException( ioe.getMessage(), ioe );
}
}
}
}
else
{
// If we don't have ldif files, we won't have sub-directories
return;
}
// Second, recurse on the sub directories
File[] dirs = entryDir.listFiles( dirFilter );
if ( ( dirs != null ) && ( dirs.length != 0 ) )
{
for ( File f : dirs )
{
loadEntries( f );
}
}
}
/**
* Create the file name from the entry Dn.
*/
private File getFile( Dn entryDn, boolean create ) throws LdapException
{
String parentDir = null;
String rdnFileName = null;
if ( entryDn.equals( suffixDn ) )
{
parentDir = suffixDirectory.getParent() + File.separator;
rdnFileName = suffixDn.getName() + CONF_FILE_EXTN;
}
else
{
StringBuilder filePath = new StringBuilder();
filePath.append( suffixDirectory ).append( File.separator );
Dn baseDn = entryDn.getDescendantOf( suffixDn );
int size = baseDn.size();
for ( int i = 0; i < size - 1; i++ )
{
rdnFileName = getFileName( baseDn.getRdn( size - 1 - i ) );
filePath.append( rdnFileName ).append( File.separator );
}
rdnFileName = getFileName( entryDn.getRdn() ) + CONF_FILE_EXTN;
parentDir = filePath.toString();
}
File dir = new File( parentDir );
if ( !dir.exists() && create )
{
// We have to create the entry if it does not have a parent
if ( !dir.mkdir() )
{
throw new LdapException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, dir ) );
}
}
File ldifFile = new File( parentDir + rdnFileName );
if ( ldifFile.exists() && create )
{
// The entry already exists
throw new LdapException( I18n.err( I18n.ERR_633 ) );
}
return ldifFile;
}
/**
* Compute the real name based on the Rdn, assuming that depending on the underlying
* OS, some characters are not allowed.
*
* We don't allow filename which length is > 255 chars.
*/
private String getFileName( Rdn rdn ) throws LdapException
{
StringBuilder fileName = new StringBuilder( "" );
Iterator<Ava> iterator = rdn.iterator();
while ( iterator.hasNext() )
{
Ava ava = iterator.next();
// First, get the AT name, or OID
String normAT = ava.getNormType();
AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
String atName = at.getName();
// Now, get the normalized value
String normValue = null;
if ( at.getSyntax().isHumanReadable() )
{
normValue = ava.getValue().getString();
}
else
{
normValue = Strings.utf8ToString( ava.getValue().getBytes() );
}
fileName.append( atName ).append( "=" ).append( normValue );
if ( iterator.hasNext() )
{
fileName.append( "+" );
}
}
return getOSFileName( fileName.toString() );
}
/**
* Compute the real name based on the Dn, assuming that depending on the underlying
* OS, some characters are not allowed.
*
* We don't allow filename which length is > 255 chars.
*/
private String getFileName( Dn dn ) throws LdapException
{
StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for ( Rdn rdn : dn.getRdns() )
{
// First, get the AT name, or OID
String normAT = rdn.getNormType();
AttributeType at = schemaManager.lookupAttributeTypeRegistry( normAT );
String atName = at.getName();
// Now, get the normalized value
String normValue = rdn.getAva().getValue().getString();
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( "," );
}
sb.append( atName ).append( "=" ).append( normValue );
}
return getOSFileName( sb.toString() );
}
/**
* Get a OS compatible file name. We URL encode all characters that may cause trouble
* according to http://en.wikipedia.org/wiki/Filenames. This includes C0 control characters
* [0x00-0x1F] and 0x7F, see http://en.wikipedia.org/wiki/Control_characters.
*/
private String getOSFileName( String fileName )
{
StringBuilder sb = new StringBuilder();
for ( char c : fileName.toCharArray() )
{
switch ( c )
{
case 0x00:
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 0x07:
case 0x08:
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C:
case 0x0D:
case 0x0E:
case 0x0F:
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F:
case 0x7F:
case ' ': // 0x20
case '"': // 0x22
case '%': // 0x25
case '&': // 0x26
case '(': // 0x28
case ')': // 0x29
case '*': // 0x2A
case '+': // 0x2B
case '/': // 0x2F
case ':': // 0x3A
case ';': // 0x3B
case '<': // 0x3C
case '>': // 0x3E
case '?': // 0x3F
case '[': // 0x5B
case '\\': // 0x5C
case ']': // 0x5D
case '|': // 0x7C
sb.append( "%" ).append( Strings.dumpHex( ( byte ) ( c >> 4 ) ) )
.append( Strings.dumpHex( ( byte ) ( c & 0xF ) ) );
break;
default:
sb.append( c );
break;
}
}
return Strings.toLowerCaseAscii( sb.toString() );
}
/**
* Write the new entry on disk. It does not exist, as this has been checked
* by the ExceptionInterceptor.
*/
private void addEntry( Entry entry ) throws LdapException
{
// Remove the EntryDN
entry.removeAttributes( entryDnAT );
try ( Writer fw = Files.newBufferedWriter( getFile( entry.getDn(), CREATE ).toPath(), StandardCharsets.UTF_8 ) )
{
fw.write( LdifUtils.convertToLdif( entry ) );
}
catch ( IOException ioe )
{
throw new LdapOperationException( ioe.getMessage(), ioe );
}
}
/**
* Recursively delete an entry and all of its children. If the entry is a directory,
* then get into it, call the same method on each of the contained files,
* and delete the directory.
*/
private boolean deleteFile( File file )
{
if ( file.isDirectory() )
{
File[] files = file.listFiles();
// Process the contained files
for ( File f : files )
{
deleteFile( f );
}
// then delete the directory itself
return file.delete();
}
else
{
return file.delete();
}
}
}