package LISM;

use strict;
use Net::LDAP::Filter;
use Net::LDAP::Constant qw(:all);
use XML::Simple;
use MIME::Base64;
use POSIX qw(strftime);
use Encode;
use Sys::Syslog;
use Sys::Syslog qw(:macros);
use LISM::Storage;
use Data::Dumper;

our $VERSION = '2.1.4';

our $lism_master = 'lism_master';
our $syncrdn = 'cn=sync';
our $master_syncrdn = 'cn=master-sync';
our $cluster_syncrdn = 'cn=cluster-sync';
our $syncInfoEntry = "objectClass: dSA\n";
our $syncInfoAttr = "description";
our $nosyncAttr = "seeAlso";
our $syncDataAttr = "ou";
our $globalLock = 'global.lock';
our $syncFailLog = 'syncfail';
our $sizeLimit = 1000000;

=head1 NAME

LISM - an OpenLDAP backend for accessing and synchronizaing data of CSV, SQL etc via LDAP

=head1 SYNOPSIS

In slapd.conf:

  database          perl
  suffix            "dc=my-domain,dc=com"
  perlModulePath    /path/to/LISM/module/files
  perlModule        LISM
  admindn           "cn=Manager,dc=my-domain,dc=com"
  adminpw           secret
  conf              /path/to/LISM/configuration/file

=head1 DESCRIPTION

When you use several RDB, LDAP and the other system, you will have a hard time synchronizing their data. LISM(LDAP Identity Synchronization Manager) solves this problem. LISM eables to update the all systems to update LDAP server of LISM.

=head1 CONSTRUCTOR

This is a plain constructor.

=cut

sub new
{
    my $class = shift;

    my $this = {};
    bless $this, $class;

    return $this;
}

=head1 METHODS

=head2 config($k, @v)

This method is called by back-perl for every configuration line. This parses XML configuration of L<LISM>.
Returns 0 if the configuration directive is valid, non-0 if it isn't.

=cut

sub config
{
    my $self = shift;
    my ($k, @v) = @_;

    if (!defined($self->{_config})) {$self->{_config} = {}}

    if ($k eq 'conf') {
        # parse XML configuration
        $self->{_lism} = XMLin($v[0], ForceArray => 1);
    } else {
        if ( @v > 1 ) {
            $self->{_config}->{$k} = \@v;
        } else {
            if ($k eq 'admindn' || $k eq 'basedn') {
                $self->{_config}->{$k} = lc $v[0];
            } else {
                $self->{_config}->{$k} = $v[0];
            }
        }
    }

    return 0;
}

=pod

=head2 init

This method is called after the configuration is parsed. This create the storage object that is needed.
Returns 0 if it complete successfully, non-0 otherwise.

=cut

sub init
{
    my $self = shift;
    my $conf;
    my $lism;

    if (!defined($self->{_config})) {$self->{_config} = {}}
    $conf = $self->{_config};

    $conf->{numloglevel} = {debug => LOG_DEBUG,
                            info => LOG_INFO,
                            notice => LOG_NOTICE,
                            warn => LOG_WARNING,
                            error => LOG_ERR,
                            crit => LOG_CRIT,
                            alert => LOG_ALERT,
                            emerg => LOG_EMERG
                           };
    if (!defined($conf->{sysloglevel})) {
        $conf->{sysloglevel} = 'debug';
    }

    if (defined($self->{_lism})) {
        $lism = $self->{_lism};
    } else {
        $self->log(level => 'alert', message => "LISM configuration doesn't exit");
        return 1;
    }

    # check configuration
    if ($self->_checkConfig()) {
        $self->log(level => 'alert', message => "slapd configuration error");
        return 1;
    }

    foreach my $dname (keys %{$self->{data}}) {
        my $data = $self->{data}{$dname};
        my $dconf = $data->{conf};
        my $module;

        foreach my $hname (keys %{$dconf->{handler}}) {
            $module = "LISM::Handler::$hname";

            eval "require $module;";
            if ($@) {
                $self->log(level => 'alert', message => "require $module: $@");
                warn $@;
                return 1;
            }

            if (!defined($self->{_handler})) {$self->{_handler} = {}};
            eval "\$self->{_handler}{$dname}{$hname} = new $module";
            if ($@) {
                $self->log(level => 'alert', message => "Can't create $module: $@");
                warn $@;
                return 1;
            }

            $dconf->{handler}{$hname}->{sysloglevel} = $conf->{sysloglevel};

            $self->{_handler}{$dname}{$hname}->config($dconf->{handler}{$hname});
            $self->{_handler}{$dname}{$hname}->init();
        }

        # load and create the storage object needed
        my ($sname) = keys %{$dconf->{storage}};
        $module = "LISM::Storage::$sname";

        eval "require $module;";
        if ($@) {
            $self->log(level => 'alert', message => "require $module: $@");
            warn $@;
            return 1;
        }

        if (!defined($self->{_storage})) {$self->{_storage} = {}};
        eval "\$self->{_storage}{$dname} = new $module(\'$data->{suffix}\', \'$data->{contentrystr}\')";
        if ($@) {
            $self->log(level => 'alert', message => "Can't create $module: $@");
            warn $@;
            return 1;
        }

        $dconf->{storage}{$sname}->{sysloglevel} = $conf->{sysloglevel};

        if ($self->{_storage}{$dname}->config($dconf->{storage}{$sname})) {
	    $self->log(level => 'alert', message => "Bad configuration of $module");
	    return 1;
        }

        if ($self->{_storage}{$dname}->init()) {
	    $self->log(level => 'alert', message => "Can't initialize $module");
	    return 1;
	}
    }

    return 0;
}

=pod

=head2 bind($binddn, $passwd)

This method is called when a client tries to bind to slapd.
Returns 0 if the authentication succeeds.

=cut

sub bind
{
    my $self = shift;
    my($binddn, $passwd) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_NO_SUCH_OBJECT;

    if (!$binddn) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    # check bind by administration user
    $binddn = lc $binddn;
    $binddn =~ s/,\s+/,/;
    if ($binddn =~ /^$self->{_config}->{admindn}$/i) {
        if (defined($self->{_config}->{adminpw}) && $passwd eq $self->{_config}->{adminpw}) {
            return LDAP_SUCCESS;
        } else {
            return LDAP_INVALID_CREDENTIALS;
        }
    }

    my $dname = $self->_getDataName($binddn);
    if (!$dname) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    # allowed to bind or not
    my $allowbind = 0;
    CHECK: {
        if (defined($dconf->{allowbind})) {
            if (defined($dconf->{allowbind}[0]->{dn})) {
                foreach my $dn (@{$dconf->{allowbind}[0]->{dn}}) {
                    if ($binddn =~ /$dn/i) {
                        $allowbind = 1;
                        last CHECK;
                    }
                }
            }

            if (defined($dconf->{allowbind}[0]->{filter})) {
                my @entries = ();
                ($rc, @entries) = $self->_do_search($binddn, 0, 0, 1, 0, $dconf->{allowbind}[0]->{filter}[0], 0);
                if ($rc) {
                    return $rc;
                } elsif (@entries) {
                    $allowbind = 1;
                    last CHECK;
                }
            }
        }
    }

    if (!$allowbind) { 
        return LDAP_INSUFFICIENT_ACCESS;
    }

    # call bind of the appropriate storage
    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        # do pre handler
        $rc = $self->_doHandler('pre', 'bind', $dname, \$binddn);

        if (!$rc) {
            $rc = $storage->bind($binddn, $passwd);
        }

        if (!$rc) {
            # do post handler
            $self->_doHandler('bind', $dname, \$binddn);
        }
    }

    if ($rc < 0) {
        $rc = LDAP_OTHER;
    }

    return $rc;
}

=pod

=head2 search($base, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs)

This method is called when a client tries to search to slapd.
Returns 0 if it completes successfully.

=cut

sub search
{
    my $self = shift;
    my($base, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs) = @_;
    my $conf = $self->{_lism};

    if (defined($conf->{sync})) {
        if ($base =~ /^($syncrdn|$master_syncrdn|$cluster_syncrdn),$self->{_config}->{basedn}$/i) {
            # get information of synchronization
            return $self->_getSyncInfo($base, $scope, $filterStr, $attrOnly, @attrs);
        }
    }

    return $self->_do_search($base, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs);
}

=pod

=head2 compare($dn, $avaStr)

This method is called when a client tries to compare to slapd.
Returns 6 if the compared value exist, 5 if it doesn't exist.

=cut

sub compare
{
    my $self = shift;
    my ($dn, $avaStr) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_NO_SUCH_OBJECT;

    $dn = lc $dn;
    $dn =~ s/,\s+/,/g;

    if ($dn eq $self->{_config}{basedn}) {
        # basedn can't be compared
        return LDAP_UNWILLING_TO_PERFORM;
    }

    my $dname = $self->_getDataName($dn);
    if (!$dname) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    # call compare of the appropriate storage
    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        # do pre handler
        $rc = $self->_doHandler('pre', 'compare', \$dn, \$avaStr);

        if (!$rc) {
            $rc = $storage->compare($dn, $avaStr);
        }

        if (!$rc) {
            # do post handler
            $self->_doHandler('post', 'compare', \$dn, \$avaStr);
        }
    }

    if ($rc < 0) {
        $rc = LDAP_OTHER;
    }

    return $rc;
}

=pod

=head2 modify($dn, @list)

This method is called when a client tries to modify to slapd. This can modify all storages required in configuration.
Returns 0 if it modify the data of one storage or all storages successfully.

=cut

sub modify
{
    my $self = shift;
    my ($dn, @list) = @_;
    my $conf = $self->{_lism};
    my $rc;

    $dn = lc $dn;
    $dn =~ s/,\s+/,/g;

    if ($dn eq $self->{_config}{basedn}) {
        # basedn can't be modifed
        return LDAP_UNWILLING_TO_PERFORM;
    }

    if (defined($conf->{sync})) {
        if ($dn =~ /^($syncrdn|$master_syncrdn|$cluster_syncrdn),$self->{_config}{basedn}$/i) {
            # set information of synchronization
            return $self->_setSyncInfo($dn, @list);
        }
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 1);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return LDAP_OPERATIONS_ERROR;
    }
    $rc = $self->_doUpdate('modify', undef, 1, $dn, @list);
    $self->_unlock($lock);

    return $rc;
}

=pod

=head2 add($entryStr)

This method is called when a client tries to add to slapd. This can add the data to all storages required in coufiguration.
Returns 0 if it add the data of one storage or all storages.

=cut

sub add
{
    my $self = shift;
    my ($entryStr) = @_;
    my $conf = $self->{_lism};
    my $rc;

    $entryStr =~ s/\n\s*([^:]+)\n/$1\n/g;
    my ($dn) = ($entryStr =~ /^dn: (.*)$/m);
    $entryStr =~ s/^dn:.*\n//;
    $dn = lc $dn ;
    $dn =~ s/,\s+/,/g;

    if ($dn eq $self->{_config}{basedn}) {
        # basedn already exist
        return LDAP_ALREADY_EXISTS;
    }

    # decode base64
    while ($entryStr =~ /^([^:]+):\:\s*(.+)$/m) {
        my $attr = $1;
        my $decoded = decode_base64($2);
        $entryStr =~ s/^$attr:\:\s*.+$/$attr: $decoded/m;
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 1);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return LDAP_OPERATIONS_ERROR;
    }
    $rc = $self->_doUpdate('add', undef, 1, $dn, $entryStr);
    $self->_unlock($lock);

    return $rc;
}

=pod

=head2 modrdn($dn, $newrdn, $delFlag)

This method is called when a client tries to modrdn to slapd. This can move the data in the storage required in coufiguration but can't do it between two storages.
Returns 0 if it move the data in one storage or all storages successfully.

=cut

sub modrdn
{
    my $self = shift;
    my ($dn, $newrdn, $delFlag) = @_;
    my $conf = $self->{_lism};
    my $rc;

    $dn = lc $dn;
    $dn =~ s/,\s+/,/g;

    if ($dn eq $self->{_config}{basedn}) {
        return LDAP_NOT_ALLOWED_ON_NONLEAF;
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 1);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return LDAP_OPERATIONS_ERROR;
    }
    $rc = $self->_doUpdate('modrdn', undef, 1, $dn, $newrdn, $delFlag);
    $self->_unlock($lock);

    return $rc;
}

=pod

=head2 delete($dn)

This method is called when a client tries to delete to slapd. This can delete the data of all storages required in configureation.
Returns 0 if it delete the data of one storage or all storages successfully.

=cut

sub delete
{
    my $self = shift;
    my ($dn) = @_;
    my $conf = $self->{_lism};
    my $rc;

    $dn = lc $dn ;
    $dn =~ s/,\s+/,/g;

    if ($dn eq $self->{_config}{basedn}) {
        return LDAP_NOT_ALLOWED_ON_NONLEAF;
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 1);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return LDAP_OPERATIONS_ERROR;
    }
    $rc = $self->_doUpdate('delete', undef, 1, $dn);
    $self->_unlock($lock);

    return $rc;
}

=pod

=head2 log(level, message)

log message to syslog.

=cut

sub log
{
    my $self = shift;
    my $conf = $self->{_config};
    my %p = @_;

    openlog('LISM', 'pid', 'local4');
    setlogmask(Sys::Syslog::LOG_UPTO($conf->{numloglevel}{$conf->{sysloglevel}}));
    syslog($conf->{numloglevel}{$p{'level'}}, $p{'message'});
    closelog
}

=pod

=head2 _checkCofig($conf, $lism)

check LISM configuration.

=cut

sub _checkConfig
{
    my $self = shift;
    my $conf = $self->{_config};
    my $lism = $self->{_lism};

    if (!defined($conf->{basedn})) {
        $self->log(level => 'alert', message => "basedn doesn't exit");
        return 1;
    }
    if (defined($conf->{syncdir}) && !-d $conf->{syncdir}) {
        $self->log(level => 'alert', message => "syncdir doesn't exit");
        return 1;
    }

    $self->{data} = {};
    foreach my $dname (keys %{$lism->{data}}) {
        my $dconf = $lism->{data}{$dname};

        # set containers
        if (!defined($dconf->{container}) || !defined($dconf->{container}[0]->{rdn})) {
            $self->log(level => 'alert', message => "$dname data container entry is invalid");
            return 1;
        }
        # normalize dn
        $self->{data}{$dname}->{suffix} = lc $dconf->{container}[0]->{rdn}[0].','.$conf->{basedn};

        # set container entry
        my $entry;
        if (!($entry = LISM::Storage->buildEntryStr($conf->{basedn}, $dconf->{container}[0]))) {
            $self->log(level => 'alert', message => "$dname data container entry is invalid");
            return 1;
        }
        $self->{data}{$dname}->{contentrystr} = $entry;

        $self->{data}{$dname}->{conf} = $dconf;
    }

    if (defined($lism->{sync})) {
        my $sync = $lism->{sync}[0];

        if (defined($sync->{master})) {
            if (!defined($sync->{master}[0]->{containerdn})) {
                $self->log(level => 'alert', message => "containerdn doesn't exist");
                return 1;
            }

            $self->{data}{$lism_master} = {};
            my $master = $self->{data}{$lism_master};
            my $src_data = $self->{data}{$sync->{master}[0]->{data}[0]};

            # normalize dn
            my $master_suffix = $sync->{master}[0]->{containerdn}[0].','.$conf->{basedn};
            $master->{suffix} = lc $master_suffix;
            ($master->{contentrystr} = $src_data->{contentrystr}) =~ s/$src_data->{suffix}$/$master_suffix/mi;
            $master->{conf} = $src_data->{conf};
        }

        foreach my $dname (keys %{$sync->{data}}) {
            if ($dname eq $lism_master) {
                $self->log(level => 'alert', message => "Data name is reserved");
	        return 1;
            }

            if (!defined($lism->{data}{$dname})) {
                $self->log(level => 'alert', message => "Data of synchronization doesn't exist");
                return 1;
            }

            my $sdata = $sync->{data}{$dname};

            # synchronization operation
            if (!defined($sdata->{syncop})) {
                $sdata->{syncop} = ['add', 'modify', 'delete'];
            }
            if (!defined($sdata->{masterop})) {
                $sdata->{masterop} = ['add', 'modify', 'delete'];
            }

            my %orders;
            foreach my $oname (keys %{$sdata->{object}}) {
                my $sobject = $sdata->{object}{$oname};

                # nomalize dn
                if (defined($sobject->{syncdn})) {
                    for (my $i = 0; $i < @{$sobject->{syncdn}}; $i++) {
                        $sobject->{syncdn}[$i] = lc $sobject->{syncdn}[$i];
                    }
                }
                if (defined($sobject->{masterdn})) {
                    for (my $i = 0; $i < @{$sobject->{masterdn}}; $i++) {
                        $sobject->{masterdn}[$i] = lc $sobject->{masterdn}[$i];
                    }
                }

                # set order
                my $num;
                if (defined($sobject->{order})) {
                    $num = $sobject->{order};
                } else {
                    $num = 100;
                }
                if (defined($orders{$num})) {
                    push(@{$orders{$num}}, $oname);
                } else {
                    $orders{$num} = [$oname];
                }

                # synchronization attributes
                if (defined($sobject->{syncattr})) {
                    foreach my $attr (@{$sobject->{syncattr}}) {
                        if (!defined($attr->{name})) {
                            $self->log(level => 'alert', message => "sync attribute name doesn't exist");
                            return 1;
                        }
                        push(@{$sobject->{syncattrs}}, $attr->{name}[0]);
                    }
                }
                if (defined($sobject->{masterattr})) {
                    foreach my $attr (@{$sobject->{masterattr}}) {
                        if (!defined($attr->{name})) {
                            $self->log(level => 'alert', message => "master attribute name doesn't exist");
                            return 1;
                        }
                        push(@{$sobject->{masterattrs}}, $attr->{name}[0]);
                    }
                }
            }

            # sort object
            $sdata->{order} = [];
            foreach (sort {$a <=> $b} keys %orders) {
                push(@{$sdata->{order}}, @{$orders{$_}});
            }
        }
    }

    return LDAP_SUCCESS;
}

sub _do_search
{
    my $self = shift;
    my($base, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs) = @_;
    my $conf = $self->{_lism};
    my $filter;
    my $rc = LDAP_SUCCESS;
    my @srchbases = ();
    my @entries = ();

    if ($base =~ /^$self->{_config}->{basedn}$/i) {
        if ($scope != 0) {
            # scope isn't base
            foreach my $dname (keys %{$self->{data}}) {
                push(@srchbases, $self->{data}{$dname}->{suffix});
            }
        }

        if ($scope == 1) {
            # scope is one
            $scope = 0;
        } else {
            my $rdn = $base;
            $rdn =~ s/^([^=]+)=([^,]+),.*/$1: $2/;
            my $entry = "dn: $base\nobjectclass: top\n$rdn\n";
            my $filter = Net::LDAP::Filter->new($filterStr);
            if (!defined($filter)) {
                return (LDAP_FILTER_ERROR, ());
            }

            if (LISM::Storage->parseFilter($filter, $entry)) {
                push(@entries, $entry);
                $sizeLim--;
            }
        }
    } else {
        push(@srchbases, $base);
    }

    if (@srchbases != 0) {
        $rc = LDAP_NO_SUCH_OBJECT;
    }

    foreach my $srchbase (@srchbases) {
        my @subentries;

        my $dname = $self->_getDataName($srchbase);
        if (!$dname) {
            return LDAP_NO_SUCH_OBJECT;
        }

        my $dconf = $self->{data}{$dname}->{conf};

        my $storage = $self->_getStorage($dname);
        if (!defined($storage)) {
            next;
        }

        # do pre handler
        $rc = $self->_doHandler('pre', 'search', $dname, \$srchbase, \$filterStr);
        if ($rc) {
            last;
        }

        # call search of the appropriate storage
        ($rc, @subentries) = $storage->search($srchbase, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs);
        if ($rc == LDAP_SERVER_DOWN) {
            $self->log(level => 'error', message => "Searching in $dname failed($rc)");
            $rc = LDAP_SUCCESS;
            next;
        } elsif ($rc) {
            last;
        }

        if (!$rc) {
            # do post handler
            $self->_doHandler('post', 'search', $dname, \@subentries);
        }

        push(@entries, @subentries);
        $sizeLim = $sizeLim - @entries;
    }

    if ($rc < 0) {
        $rc = LDAP_OTHER;
    }

    return ($rc, @entries);
}

sub _doUpdate
{
    my $self = shift;
    my ($func, $src_data, $commit, $dn, @info) = @_;
    my $method = "_do_$func";
    my @updated;
    my $rc = LDAP_SUCCESS;

    if ($dn =~ /^[^,]*,$self->{_config}{basedn}$/) {
        # can't update entry under basedn
        return LDAP_UNWILLING_TO_PERFORM;
    }

    # add timestamp for openldap 2.3(backward compatibility)
    if ($func eq 'add') {
        if ($info[0] !~ /^createtimestamp:/mi) {
            my $ts = strftime("%Y%m%d%H%M%S", localtime(time))."Z";
            $info[0] = $info[0]."createtimestamp: $ts\nmodifytimestamp: $ts\n";
        }
    } elsif ($func eq 'modify') {
        if (!grep(/^modifytimestamp$/, @info)) {
            my $ts = strftime("%Y%m%d%H%M%S", localtime(time))."Z";
            push(@info, 'REPLACE', 'modifytimestamp', $ts);
        }
    }

    my $dname = $self->_getDataName($dn);
    if (!$dname) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};
    if (defined($dconf->{readonly}) && $dconf->{readonly}[0] =~ /^on$/i) {
        return LDAP_UNWILLING_TO_PERFORM;
    }

    if (!$self->_checkSync($dname) || $commit) {
        # do pre handler
        $rc = $self->_doHandler('pre', $func, $dname, \$dn, \@info);
    }

    # replicate the udpate operation to the storages
    if ($self->_checkSync($dname) && $commit) {
        ($rc, @updated) = $self->_doSync($func, $src_data, $dn, @info);
    } else {
        if (!$rc && !($rc = $self->$method($dname, $dn, @info))) {
            push(@updated, $dname);
        }
    }

    if (!$rc && (!$self->_checkSync($dname) || $commit)) {
        # do post handler
        $self->_doHandler('post', $func, $dname, \$dn, \@info);
    }

    if ($commit && @updated) {
        if ($rc) {
            $self->_updateRollback(@updated);
        } else {
            $self->_updateCommit(@updated);
        }
    }

    if ($rc < 0) {
        $rc = LDAP_OTHER;
    }

    if (!$self->_checkSync($dname) || $commit) {
        $self->log(level => 'notice', message => $self->_auditMsg($func, $dn, $rc, @info));
    }

    return $rc;
}

sub _do_modify
{
    my $self = shift;
    my ($dname, $dn, @list) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_SUCCESS;

    if (!defined($self->{data}{$dname})) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    # call modify of the appropriate storage
    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        my @mod_list;

        while ( @list > 0 && !$rc) {
            my $action = shift @list;
            my $key    = lc(shift @list);
            my @values;

            while (@list > 0 && $list[0] ne "ADD" && $list[0] ne "DELETE" && $list[0] ne "REPLACE") {
                push(@values, shift @list);
            }

            if ($key =~ /^userpassword$/i && @values && $values[0]) {
                # hash the password in the modification data
                $values[0] = $storage->hashPasswd($values[0]);
                if (!defined($values[0])) {
                    next;
                }
            }

            push(@mod_list, ($action, $key, @values));
        }

        if (@mod_list) {
            $rc = $storage->modify($dn, @mod_list);
        }
    }

    return $rc;
}

sub _do_add
{
    my $self = shift;
    my ($dname, $dn, $entryStr) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_SUCCESS;

    if (!defined($self->{data}{$dname})) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    # call add of the appropriate storage
    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        # hash the password in the entry
        if ($entryStr =~ /^userpassword:\s*(.+)$/mi) {
            my $hash = $storage->hashPasswd($1);
            if (defined($hash)) {
                $entryStr =~ s/^userpassword:.*$/userpassword: $hash/mi;
            } else {
                $entryStr =~ s/\nuserpassword:.*\n/\n/i;
            }
        }

        $rc = $storage->add($dn, $entryStr);
    }

    return $rc;
}

sub _do_modrdn
{
    my $self = shift;
    my ($dname, $dn, $newrdn, $delFlag) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_SUCCESS;

    if (!defined($self->{data}{$dname})) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        $rc = $storage->modrdn($dn, $newrdn, $delFlag);
    }

    return $rc;
}

sub _do_delete
{
    my $self = shift;
    my ($dname, $dn) = @_;
    my $conf = $self->{_lism};
    my $rc = LDAP_SUCCESS;

    if (!defined($self->{data}{$dname})) {
        return LDAP_NO_SUCH_OBJECT;
    }

    my $dconf = $self->{data}{$dname}->{conf};

    # call delete of the appropriate storage
    my $storage = $self->_getStorage($dname);
    if (defined($storage)) {
        $rc = $storage->delete($dn);
    }

    return $rc;
}

sub _doHandler
{
    my $self = shift;
    my ($type, $func, $dname, @args) = @_;
    my $method = $type.'_'.$func;
    my $rc = LDAP_SUCCESS;

    if (!defined($self->{_handler}{$dname})) {
        return 0;
    }

    foreach my $order ('first', 'middle', 'last') {
        foreach my $hname (keys %{$self->{_handler}{$dname}}) {
            if ($self->{_handler}{$dname}{$hname}->getOrder() ne $order) {
                next;
            }

            $rc = $self->{_handler}{$dname}{$hname}->$method(@args);
            if ($rc) {
                return LDAP_OTHER;
            }
        }
    }

    return $rc;
}

sub _checkSync
{
    my $self = shift;
    my ($dname) = @_;
    my $conf = $self->{_lism};

    if ($dname eq $lism_master) {
        return 1;
    }

    return 0;
}

sub _doSync
{
    my $self = shift;
    my ($func, $src_data, $dn, @info) = @_;
    my $conf = $self->{_lism};
    my $sync = $conf->{sync}[0];
    my $master = $self->{data}{$lism_master};
    my @updated = ();
    my $rc = LDAP_SUCCESS;

    my $entryStr;
    if ($func eq 'add') {
        $entryStr = $info[0];
    } else {
        # check entry existence
        my @entries;
        ($rc, @entries) = $self->_do_search($dn, 2, 0, 0, 0, '(objectClass=*)');
        if ($rc) {
            $self->log(level => 'error', message => "Getting synchronized entry($dn) failed: error code($rc)");
            return($rc, @updated);
        }
        ($entryStr = $entries[0]) =~ s/^dn:.*\n//;
    }

    # update the master storage
    if ($func ne 'delete') {
        $rc = $self->_doUpdate($func, undef, 0, $dn, @info);
        if ($rc) {
            $self->log(level => 'error', message => "Updating master entry($dn) failed: error code($rc)");
            return($rc, @updated);
        }
    }

    foreach my $dname (keys %{$sync->{data}}) {
        if ($src_data && $src_data eq $dname) {
            next;
        }

        my $ddn = $dn;
        my $data = $self->{data}{$dname};
        my @dinfo = ();
        my $sdata = $sync->{data}{$dname};
        my $sobject;
        my $sbase;
        my %ops;
        $ops{add} = 0;
        $ops{modify} = 0;
        $ops{delete} = 0;

        foreach my $op (@{$sdata->{syncop}}) {
            $ops{$op} = 1;
        }

        # operation should be synchronized or not
        if (!$ops{$func}) {
            next;
        }

        # get object synchronized
        foreach my $oname (keys %{$sdata->{object}}) {
            if (!defined($sdata->{object}{$oname}->{syncdn})) {
                next;
            }

            foreach my $syncdn (@{$sdata->{object}{$oname}->{syncdn}}) {
                $sbase = $syncdn.','.$master->{suffix};
                if ($dn =~ /,$sbase$/i) {
                    $sobject = $sdata->{object}{$oname};
                    last;
                }
            }
            if ($sobject) {
                last;
            }
        }

        if (!$sobject) {
            next;
        }

        # check need for synchronization
        if (defined($sobject->{syncfilter})) {
            my $syncfilter = Net::LDAP::Filter->new($sobject->{syncfilter}[0]);
            if (!LISM::Storage->parseFilter($syncfilter, "$dn\n$entryStr")) {
                next;
            }
        }

        # get attributes synchronized
        if ($func eq 'add') {
            my ($rdn_attr) = ($dn =~ /^([^=]+)=/);
      	    foreach my $attr ('objectClass', $rdn_attr) {
                $dinfo[0] = $dinfo[0].join("\n", ($info[0] =~ /^($attr: .*)$/gmi))."\n";
            }

            my @sync_attrs;
            if (defined($sobject->{syncattrs})) {
                @sync_attrs = @{$sobject->{syncattrs}};
            } else {
                @sync_attrs = ($info[0] =~ /^([^:]+):/gmi);
            }

            for (my $j = 0; $j < @sync_attrs; $j++) {
                my $attr = $sync_attrs[$j];
                my $sattr;
 
                if ($attr =~ /^(objectClass|$rdn_attr)$/i) {
                    next;
                }

                if (defined($sobject->{syncattr})) {
                    $sattr = $sobject->{syncattr}[$j];
                }

                my @values = $info[0] =~ /^$attr: (.*)$/gmi;
                if (!@values) {
                    next;
                }

                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);

                if (@sync_vals) {
                    foreach my $value (@sync_vals) {
                        $dinfo[0] = "$dinfo[0]$attr: $value\n";
                    }
                }
            }

            if (!$dinfo[0]) {
                next;
            }
        } elsif ($func eq 'modify') {
            my @tmp = @info;
            while (@tmp > 0) {
                my $action = shift @tmp;
                my $attr   = lc(shift @tmp);
                my @values;
                my $sattr;

                while (@tmp > 0 && $tmp[0] ne "ADD" && $tmp[0] ne "DELETE" && $tmp[0] ne "REPLACE") {
                    push(@values, shift @tmp);
                }

                if (defined($sobject->{syncattrs})) {
                    for (my $i = 0; $i < @{$sobject->{syncattrs}}; $i++) {
                        if ($attr =~ /^$sobject->{syncattrs}[$i]$/i) {
                            $sattr = $sobject->{syncattr}[$i];
                            last;
                        }
                    }

                    if (!$sattr) {
                        next;
                    }
                }

                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                if (@sync_vals) {
                    push(@dinfo, $action, $attr, @sync_vals);
                } elsif ($action eq "DELETE" && !@values) {
                    push(@dinfo, $action, $attr);
                }
            }

            if (!@dinfo) {
                next;
            }
        } else {
            @dinfo = @info;
        }

        # replace dn to dn in the data entry
        $ddn =~ s/$master->{suffix}$/$data->{suffix}/i;
        $ddn = lc($ddn);

        # replicate to the storage
        $rc = $self->_doUpdate($func, undef, 0, $ddn, @dinfo);
        if ($rc == LDAP_NO_SUCH_OBJECT && $func eq 'delete' || $func eq 'modify'
            && !$ops{add}) {
            next;
        }

        if ($rc) {
            $self->log(level => 'error', message => "Synchronizing $dname failed: error code($rc)");

            if ($sync->{transaction}[0] =~ /^on$/i) {
                last;
            }

            $self->_writeSyncFail($func, $dname, $ddn, @dinfo);
        } else {
            push(@updated, $dname);
        }
    }

    # Delete master entry last for ldap rewrite map
    if ($func eq 'delete') {
        $rc = $self->_doUpdate($func, undef, 0, $dn, @info);
        if ($rc) {
            $self->log(level => 'error', message => "Updating master entry($dn) failed: error code($rc)");
            return($rc, @updated);
        }
    }

    if ($sync->{transaction}[0] !~ /^on$/i) {
        $rc = LDAP_SUCCESS;
    }

    return ($rc, @updated);
}

sub _updateCommit
{
    my $self = shift;
    my (@updated) = @_;

    for (my $i = 0; $i < @updated; $i++) {
        $self->{_storage}{$updated[$i]}->commit();
    }
}

sub _updateRollback
{
    my $self = shift;
    my (@updated) = @_;

    for (my $i = 0; $i < @updated; $i++) {
        $self->{_storage}{$updated[$i]}->rollback();
    }
}

sub _getSyncInfo
{
    my $self = shift;
    my ($base, $scope, $filterStr, $attrOnly, @attrs) = @_;
    my $conf = $self->{_lism};
    my $sync = $conf->{sync}[0];
    my $master = $self->{data}{$lism_master};
    my $present_list;
    my @check_data = ();
    my $syncStatus = '';
    my %nosync_data;
    my %nosync_entries;
    my %deletedn;

    # don't return entry when the scope isn't base
    if ($scope != 0) {
        return (0, ());
    }

    # get checked data
    my (@check_dnames) = ($filterStr =~ /\($syncDataAttr=(.*)\)/gi);
    if (@check_dnames) {
        foreach my $dname (keys %{$sync->{data}}) {
            if (grep(/$dname/i, @check_dnames)) {
                push(@check_data, $dname);
            }
        }
    } else {
        @check_data = keys %{$sync->{data}};
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 2);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return (LDAP_OPERATIONS_ERROR, ());
    }

    my $syncentry;
    ($syncentry = $base) =~ s/^([^=]+)=([^,]+),.*/$1: $2/;
    $syncentry = "dn: $base\n$syncInfoEntry$syncentry\n";

    # get present entry list
    $present_list = $self->_getPresentList();
    if ($base !~ /^$cluster_syncrdn/) {
        # check master data
        foreach my $dname (@check_data) {
            my $data = $self->{data}{$dname};
            my $sdata = $sync->{data}{$dname};

            foreach my $oname (@{$sdata->{order}}) {
	        my $sobject = $sdata->{object}{$oname};
                my %ops;
                $ops{add} = 0;
                $ops{modify} = 0;
                $ops{delete} = 0;

                if (!defined($sobject->{masterdn})) {
                    next;
                }

                foreach my $masterdn (@{$sobject->{masterdn}}) {
                    my $dbase = $masterdn.','.$data->{suffix};
                    my $sbase = $masterdn.','.$master->{suffix};

                    foreach my $op (@{$sdata->{masterop}}) {
                        $ops{$op} = 1;
                        if ($op eq 'delete') {
                            $deletedn{$sbase} = [$dname, $oname];
                        }
                    }

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dbase, 2, 0, $sizeLimit, 0, 'objectClass=*', 0);
                    if ($rc) {
                        $self->log(level => 'error', message => "Can't get values of $dname($rc)");
                        return ($rc, ());
                    }

                    # synchronization filter
                    my $masterfilter = undef;
                    if (defined($sobject->{masterfilter})) {
                        $masterfilter = Net::LDAP::Filter->new($sobject->{masterfilter}[0]);
                    }

                    # synchronized attributes
                    my @sync_attrs;
                    if (defined($sobject->{masterattrs})) {
                        @sync_attrs = @{$sobject->{masterattrs}};
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        my ($key) = ($dn =~ /^(.*),$dbase$/);
                        if (!$key) {
                            next;
                        }
                        $key = lc($key);

                        # check need for synchronization
                        if (defined($masterfilter) &&
                            !LISM::Storage->parseFilter($masterfilter, $entries[$i])) {
                            next;
                        }

                        if (!defined($present_list->{$sbase}{$key})) {
                            # data storage's entry doesn't exist in master storage
                            if ($ops{add}) {
                                $nosync_data{$dname} = 1;
                                $nosync_entries{lc($dn)} = 1;
                            }
                        } else {
                            if (!defined($sobject->{masterattrs})) {
                                @sync_attrs = $self->_unique(($present_list->{$sbase}{$key}->{entryStr} =~ /^([^:]+):/gmi), ($entries[$i] =~ /\n([^:]+):/gi));
                            }

                            for (my $j = 0; $j < @sync_attrs; $j++) {
                                my $attr = $sync_attrs[$j];
                                my $sattr;
                                my @values;

                                if (defined($sobject->{masterattr})) {
                                    $sattr = $sobject->{masterattr}[$j];
                                }

                                @values = $entries[$i] =~ /^$attr: (.*)$/gmi;
                                my @sync_vals = $self->_checkSyncAttrs($data, $master, $sattr, @values);
                                my $dvals = join("\n", sort(@sync_vals));

                                @values = $present_list->{$sbase}{$key}->{entryStr} =~ /^$attr: (.*)$/gmi;
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($master, $data, $sattr, @values);
                                my $pvals = join("\n", @{$synced_vals});

                                # ignore passowrd equality if hash type is differnt
                                if ($attr =~ /^userpassword$/i &&
                                    !$self->_cmpPwdHash($dvals, $pvals)) {
                                    next;
                                }

                                $pvals =~ s/([.*+?\[\]])/\\$1/g;
                                if ($dvals !~ /^$pvals$/i && $ops{modify}) {
                                    $nosync_data{$dname} = 1;
                                    $nosync_entries{lc($dn)} = 1;
                                    last;
                                }
                            }
                        }

                        if ($ops{delete}) {
                            $present_list->{$sbase}{$key}->{present} = 1;
                        }
	            }
                }
            }
        }

        # check deleted entry in present list
        foreach my $sbase (keys %deletedn) {
            my $sobject = $sync->{data}{$deletedn{$sbase}[0]}->{object}{$deletedn{$sbase}[1]};

            # synchronization filter
            my $masterfilter = undef;
            if (defined($sobject->{masterfilter})) {
                $masterfilter = Net::LDAP::Filter->new($sobject->{masterfilter}[0]);
            }

            foreach my $key (keys %{$present_list->{$sbase}}) {
                if (!$present_list->{$sbase}{$key}->{present}) {
                    if (defined($masterfilter) &&
                        !LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{$key}->{entryStr})) {
                        next;
                    }
                    $nosync_entries{lc("$key,$sbase")} = 1;
                }
            }
        }
    }

    if ($base !~ /^$master_syncrdn/) {
        # check sync data
        foreach my $dname (@check_data) {
            my $data = $self->{data}{$dname};
            my $sdata = $sync->{data}{$dname};

            foreach my $oname (@{$sdata->{order}}) {
                my $sobject = $sdata->{object}{$oname};
                my %ops;
                $ops{add} = 0;
                $ops{modify} = 0;
                $ops{delete} = 0;

                if (!defined($sobject->{syncdn})) {
                    next;
                }

                foreach my $op (@{$sdata->{syncop}}) {
                    $ops{$op} = 1;
                }

                foreach my $syncdn (@{$sobject->{syncdn}}) {
                    my $dbase = $syncdn.','.$data->{suffix};
                    my $sbase = $syncdn.','.$master->{suffix};

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dbase, 2, 0, $sizeLimit, 0, 'objectClass=*', 0);
                    if ($rc) {
                        $self->log(level => 'error', message => "Can't get values of $dname($rc)");
                        return ($rc, ());
                    }

                    # synchronization filter
                    my $syncfilter = undef;
                    if (defined($sobject->{syncfilter})) {
                        $syncfilter = Net::LDAP::Filter->new($sobject->{syncfilter}[0]);
                    }

                    # synchronized attributes
                    my @sync_attrs;
                    if (defined($sobject->{syncattrs})) {
                        @sync_attrs = @{$sobject->{syncattrs}};
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        my ($key) = ($dn =~ /^(.*),$dbase$/);
                        if (!$key) {
                            next;
                        }
                        $key = lc($key);

                        if (!defined($present_list->{$sbase}{$key})) {
                            # data storage's entry doesn't exist in master storage
                            if ($ops{delete}) {
                                $nosync_data{$dname} = 1;
                                $nosync_entries{lc($dn)} = 1;
                            }
                        } elsif ($present_list->{$sbase}{$key} && !defined($syncfilter) ||
                                LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{$key}->{entryStr})) {
                            if (!defined($sobject->{syncattrs})) {
                                @sync_attrs = $self->_unique(($present_list->{$sbase}{$key}->{entryStr} =~ /^([^:]+):/gmi), ($entries[$i] =~ /\n([^:]+):/gi));
                            }

                            for (my $j = 0; $j < @sync_attrs; $j++) {
                                my $attr = $sync_attrs[$j];
                                my $sattr;
                                my @values;

                                if (defined($sobject->{syncattr})) {
                                    $sattr = $sobject->{syncattr}[$j];
                                }

                                if (defined($sattr->{check}) && $sattr->{check}[0] eq 'off') {
                                    next;
                                }

                                @values = $present_list->{$sbase}{$key}->{entryStr} =~ /^$attr: (.*)$/gmi;
                                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                my $pvals = join("\n", @sync_vals);

                                @values = $entries[$i] =~ /^$attr: (.*)$/gmi;
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($data, $master, $sattr, @values);
                                my $dvals = join("\n", sort(@{$synced_vals}));

                                # ignore passowrd equality if hash type is differnt
                                if ($attr =~ /^userpassword$/i &&
                                    !$self->_cmpPwdHash($dvals, $pvals)) {
                                    next;
                                }

                                $pvals =~ s/([.*+?\[\]])/\\$1/g;
                                if ($dvals !~ /^$pvals$/i && $ops{modify}) {
                                    $nosync_data{$dname} = 1;
                                    $nosync_entries{lc($dn)} = 1;
                                    last;
                                }
                            }
                        }

                        if ($ops{add} && defined($present_list->{$sbase}{$key})) {
                            $present_list->{$sbase}{$key}->{sync_present}{$dname} = 1;
                        }
                    }

                    # check added entry in present list
                    if ($ops{add}) {
                        foreach my $key (keys %{$present_list->{$sbase}}) {
                            if (!defined($present_list->{$sbase}{$key}->{sync_present}{$dname}) &&
                                (!defined($syncfilter) ||
                                    LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{$key}->{entryStr}))) {
                                $nosync_data{$dname} = 1;
                                $nosync_entries{lc("$key,$dbase")} = 1;
                            }
                        }
                    }
                }
            }
        }
    }

    $self->_unlock($lock);

    if (!%nosync_entries) {
        $syncentry = $syncentry."$syncInfoAttr: sync\n";
    } else {
        $syncentry = $syncentry."$syncInfoAttr: nosync\n";
        foreach my $dname (keys %nosync_data) {
            $syncentry = $syncentry."$syncDataAttr: $dname\n";
        }
        foreach my $dn (keys %nosync_entries) {
            $syncentry = $syncentry."$nosyncAttr: $dn\n";
        }
    }

    return (LDAP_SUCCESS, ($syncentry));
}

sub _setSyncInfo
{
    my $self = shift;
    my ($dn, @list) = @_;
    my $conf = $self->{_lism};
    my $sync = $conf->{sync}[0];
    my $master = $self->{data}{$lism_master};
    my $present_list;
    my @sync_data = ();
    my %deletedn;
    my $rc = LDAP_SUCCESS;

    my $modinfo = join(',', @list);

    # get synchronized data
    my ($sync_dnames) = ($modinfo =~ /DELETE,$syncDataAttr,(.*),?(ADD|DELETE|REPLACE|)/i);
    if ($sync_dnames) {
        foreach my $dname (keys %{$sync->{data}}) {
            if (",$sync_dnames," =~ /$dname,/i) {
                push(@sync_data, $dname);
            }
        }
    } else {
        if ($modinfo !~ /REPLACE,$syncInfoAttr,sync/i) {
            return LDAP_UNWILLING_TO_PERFORM;
        }
        @sync_data = keys %{$sync->{data}};
    }

    my $lock = $self->_lock($self->{_config}->{syncdir}.'/'.$globalLock, 2);
    if (!defined($lock)) {
        $self->log(level => 'crit', message => "Can't get global lock");
        return LDAP_OPERATIONS_ERROR;
    }

    if ($dn !~ /^$cluster_syncrdn/) {
        # get present entry list
        $present_list = $self->_getPresentList();

        foreach my $dname (@sync_data) {
            my $data = $self->{data}{$dname};
            my $sdata = $sync->{data}{$dname};

            foreach my $oname (@{$sdata->{order}}) {
                my $sobject = $sdata->{object}{$oname};
                my %ops;
                $ops{add} = 0;
                $ops{modify} = 0;
                $ops{delete} = 0;

                if (!defined($sobject->{masterdn})) {
                    next;
                }

                foreach my $masterdn (@{$sobject->{masterdn}}) {
                    my $dbase = $masterdn.','.$data->{suffix};
                    my $sbase = $masterdn.','.$master->{suffix};

                    foreach my $op (@{$sdata->{masterop}}) {
                        $ops{$op} = 1;
                        if ($op eq 'delete') {
                            $deletedn{$sbase} = [$dname, $oname];
                        }
                    }

                    # get values from data storage
                    my ($rc, @entries) = $self->search($dbase, 2, 0, $sizeLimit, 0, 'objectClass=*', 0);
                    if ($rc) {
                        $self->log(level => 'error', message => "Can't get values of $dname($rc)");
                        return $rc;
                    }

                    # synchronization filter
                    my $masterfilter = undef;
                    if (defined($sobject->{masterfilter})) {
                        $masterfilter = Net::LDAP::Filter->new($sobject->{masterfilter}[0]);
                    }

                    # synchronized attributes
                    my @sync_attrs;
                    if (defined($sobject->{masterattrs})) {
                        @sync_attrs = @{$sobject->{masterattrs}};
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        # check need for synchronization
                        if (defined($masterfilter) &&
                            !LISM::Storage->parseFilter($masterfilter, $entries[$i])) {
                            next;
                        }

                        my ($key) = ($entries[$i] =~ /^dn: (.*),$dbase\n/);
                        if (!$key) {
                            next;
                        }
                        $key = lc($key);

                        my $dn = "$key,$sbase";
                        if (!defined($present_list->{$sbase}{$key})) {
                            # add entry which doesn't exist in master storage
                            my ($rdn_attr) = ($key =~ /^([^=]+)=/);
                            my $entryStr;

                            foreach my $attr ('objectClass', $rdn_attr) {
                                $entryStr = $entryStr.join("\n", ($entries[$i] =~ /^($attr: .*)$/gmi))."\n";
                            }

                            if (!defined($sobject->{masterattrs})) {
                                @sync_attrs = ($entries[$i] =~ /\n([^:]+):/gi);
                            }

                            for (my $j = 0; $j < @sync_attrs; $j++) {
                                my $attr = $sync_attrs[$j];
                                my $sattr;

                                if ($attr =~ /^(objectClass|$rdn_attr)$/i) {
                                    next;
                                }

                                if (defined($sobject->{masterattr})) {
                                    $sattr = $sobject->{masterattr}[$j];
                                }

                                my @values = $entries[$i] =~ /^$attr: (.*)$/gmi;
                                if (!@values) {
                                    next;
                                }

                                my @sync_vals = $self->_checkSyncAttrs($data, $master, $sattr, @values);

                                if (@sync_vals) {
                                    foreach my $value (@sync_vals) {
                                        $entryStr = "$entryStr$attr: $value\n";
                                    }
                                }
                            }

                            if ($ops{add}) {
                                $dn = encode('utf8', $dn);
                                $entryStr = encode('utf8', $entryStr);
                                $rc = $self->_doUpdate('add', $dname, 1, $dn, $entryStr);
                                $present_list->{$sbase}{$key}->{entryStr} = $entryStr;
                            }
                        } else {
                            # modify entry which isn't equal to data storage
                            my @modlist;
                            my $pocs;
                            my $entryStr = $present_list->{$sbase}{$key}->{entryStr};

                            if (!defined($sobject->{masterattrs})) {
                                @sync_attrs = $self->_unique(($present_list->{$sbase}{$key}->{entryStr} =~ /^([^:]+):/gmi), ($entries[$i] =~ /\n([^:]+):/gi));
                            }

                            for (my $j = 0; $j < @sync_attrs; $j++) {
                                my $attr = $sobject->{masterattrs}[$j];
                                my $sattr;
                                my @values;

                                if (defined($sobject->{masterattr})) {
                                    $sattr = $sobject->{masterattr}[$j];
                                }

                                @values = $entries[$i] =~ /^$attr: (.*)$/gmi;
                                my @sync_vals = $self->_checkSyncAttrs($data, $master, $sattr, @values);
                                my $dvals = join("\n", sort(@sync_vals));

                                @values = $present_list->{$sbase}{$key}->{entryStr} =~ /^$attr: (.*)$/gmi;
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($master, $data, $sattr, @values);
                                my $pvals = join("\n", @{$synced_vals});

                                # ignore passowrd equality if hash type is differnt
                                if ($attr =~ /^userpassword$/i &&
                                    !$self->_cmpPwdHash($dvals, $pvals)) {
                                    next;
                                }

                                $pvals =~ s/([.*+?\[\]])/\\$1/g;
                                if ($dvals !~ m/^$pvals$/i && $ops{modify}) {
                                    if (!$pocs) {
                                        # Add objectClass for adding new attribute
                                        $pocs = join("\n", $present_list->{$sbase}{$key}->{entryStr} =~ /^objectClass: (.*)$/gmi);
                                        my @ocs;

                                        foreach my $doc ($entries[$i] =~ /^objectClass: (.*)$/gmi) {
                                            if ($pocs !~ /^$doc$/mi) {
                                                push(@ocs, $doc);
                                            }
                                        }
                                        if (@ocs) {
                                            push(@modlist, ('ADD', 'objectClass', @ocs));
                                        }
                                    }

                                    if (@{$left_vals}) {
                                        push(@sync_vals, @{$left_vals});
                                    }

                                    if (@sync_vals) {
                                        my @mod_vals;
                                        for (my $k = 0; $k < @sync_vals; $k++) {
                                            $mod_vals[$k] = encode('utf8', $sync_vals[$k]);
                                        }
                                        push(@modlist, ('REPLACE', $attr, @mod_vals));
                                    } else {
                                        push(@modlist, ('DELETE', $attr));
                                    }

                                    $entryStr =~ s/$attr: .*\n//gi;
                                    foreach my $value (@sync_vals) {
                                        $entryStr = "$entryStr\n$attr: $value";
                                    }
                                }
		            }

                            if (@modlist) {
                                if ($ops{modify}) {
                                    $dn = encode('utf8', $dn);
                                    $rc = $self->_doUpdate('modify', $dname, 1, $dn, @modlist);
                                    $present_list->{$sbase}{$key}->{entryStr} = $entryStr;
                                }
                            }
                        }

                        if ($rc) {
                            $self->log(level => 'error', message => "Synchronizing \"$dn\" failed($rc)");
                            return $rc;
                        }

                        if ($ops{delete}) {
                            $present_list->{$sbase}{$key}->{present} = 1;
                        }
                    }
                }
            }
        }

        # delete entries which don't exist in data storages
        foreach my $sbase (keys %deletedn) {
            my $sobject = $sync->{data}{$deletedn{$sbase}[0]}->{object}{$deletedn{$sbase}[1]};

            # synchronization filter
            my $masterfilter = undef;
            if (defined($sobject->{masterfilter})) {
                $masterfilter = Net::LDAP::Filter->new($sobject->{masterfilter}[0]);
            }

            foreach my $key (keys %{$present_list->{$sbase}}) {
                if (!$present_list->{$sbase}{$key}->{present}) {
                    if (defined($masterfilter) &&
                        !LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{$key}->{entryStr})) {
                        next;
                    }

                    my $nosync_entry = "$key,$sbase";

                    $nosync_entry = encode('utf8', $nosync_entry);
                    $rc = $self->_doUpdate('delete', undef, 1, $nosync_entry);

                    if ($rc) {
                        $self->log(level => 'error', message => "Synchronizing \"$nosync_entry\" failed($rc)");
                        return $rc;
                    }
                }
            }
        }
    }

    if ($dn !~ /^$master_syncrdn/) {
        # get new present entry list
        undef $present_list;
        $present_list = $self->_getPresentList();

        foreach my $dname (@sync_data) {
            my $data = $self->{data}{$dname};
            my $sdata = $sync->{data}{$dname};

            if (!defined($sdata->{syncop})) {
                next;
            }

            foreach my $oname (@{$sdata->{order}}) {
                my $sobject = $sdata->{object}{$oname};
                my %ops;
                $ops{add} = 0;
                $ops{modify} = 0;
                $ops{delete} = 0;

                if (!defined($sobject->{syncdn})) {
                    next;
                }

                foreach my $op (@{$sdata->{syncop}}) {
                    $ops{$op} = 1;
                    if ($op eq 'delete') {
                        $deletedn{$sobject->{syncdn}[0]} = 1;
                    }
                }

                foreach my $syncdn (@{$sobject->{syncdn}}) {
                    my $dbase = $syncdn.','.$data->{suffix};
                    my $sbase = $syncdn.','.$master->{suffix};

                    # get values from data storage
                    my ($rc, @entries) = $self->search($dbase, 2, 0, $sizeLimit, 0, 'objectClass=*', 0);
                    if ($rc) {
                        $self->log(level => 'error', message => "Can't get values of $dname($rc)");
                        return $rc;
                    }

                    # synchronization filter
                    my $syncfilter = undef;
                    if (defined($sobject->{syncfilter})) {
                        $syncfilter = Net::LDAP::Filter->new($sobject->{syncfilter}[0]);
                    }

                    # synchronized attributes
                    my @sync_attrs;
                    if (defined($sobject->{syncattrs})) {
                        @sync_attrs = @{$sobject->{syncattrs}};
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        my ($key) = ($dn =~ /^(.*),$dbase$/);
                        if (!$key) {
                            next;
                        }
                        $key = lc($key);

                        if (!defined($present_list->{$sbase}{$key})) {
                            if ($ops{delete}) {
                                # delete entry which don't exist in master storage
                                $dn = encode('utf8', $dn);
                                $rc = $self->_doUpdate('delete', undef, 1, $dn);
                            }
                        } elsif (!defined($syncfilter) ||
                            LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{$key}->{entryStr})){
                            # modify entry which isn't equal to master storage
                            my @modlist;
                            my $pocs;

                            if (!defined($sobject->{syncattrs})) {
                                @sync_attrs = $self->_unique(($present_list->{$sbase}{$key}->{entryStr} =~ /^([^:]+):/gmi), ($entries[$i] =~ /\n([^:]+):/gi));
                            }

                            for (my $j = 0; $j < @sync_attrs; $j++) {
                                my $attr = $sync_attrs[$j];
                                my $sattr;
                                my @values;

                                if (defined($sobject->{syncattr})) {
                                    $sattr = $sobject->{syncattr}[$j];
                                }

                                if (defined($sattr->{check}) && $sattr->{check}[0] eq 'off') {
                                    next;
                                }

                                @values = $present_list->{$sbase}{$key}->{entryStr} =~ /^$attr: (.*)$/gmi;
                                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                my $pvals = join("\n", @sync_vals);

                                @values = $entries[$i] =~ /^$attr: (.*)$/gmi;
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($data, $master, $sattr, @values);
                                my $dvals = join("\n", sort(@{$synced_vals}));

                                # ignore passowrd equality if hash type is differnt
                                if ($attr =~ /^userpassword$/i &&
                                    !$self->_cmpPwdHash($dvals, $pvals)) {
                                    next;
                                }

                                $pvals =~ s/([.*+?\[\]])/\\$1/g;
                                if ($dvals !~ m/^$pvals$/i) {
                                    if (@{$left_vals}) {
                                        push(@sync_vals, @{$left_vals});
                                    }

                                    if (@sync_vals) {
                                        my @mod_vals;
                                        for (my $k = 0; $k < @sync_vals; $k++) {
                                            $mod_vals[$k] = encode('utf8', $sync_vals[$k]);
                                        }
                                        push(@modlist, ('REPLACE', $attr, @mod_vals));
                                    } else {
                                        push(@modlist, ('DELETE', $attr));
                                    }
                                }
                            }

                            if (@modlist) {
                                if ($ops{modify}) {
                                    $dn = encode('utf8', $dn);
                                    $rc = $self->_doUpdate('modify', undef, 1, $dn, @modlist);
                                }
                            }
                        }

                        if ($rc) {
                            $self->log(level => 'error', message => "Synchronizing \"$dn\" failed($rc)");
                            return $rc;
                        }

                        if ($ops{add} && defined($present_list->{$sbase}{$key})) {
                            $present_list->{$sbase}{$key}->{sync_present}{$dname} = 1;
                        }
                    }

                    # add entries which don't exist in data storages
                    if ($ops{add}) {
                        foreach my $key (keys %{$present_list->{$sbase}}) {
                            if (!defined($present_list->{$sbase}{$key}->{sync_present}{$dname}) &&
                                (!defined($syncfilter) ||
                                    LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{$key}->{entryStr}))) {
                                my $nosync_entry = "$key,$dbase";

                                my ($rdn_attr) = ($key =~ /^([^=]+)=/);
                                my $entryStr;

                                foreach my $attr ('objectClass', $rdn_attr) {
                                    $entryStr = $entryStr.join("\n", ($present_list->{$sbase}{$key}->{entryStr} =~ /^($attr: .*)$/gmi))."\n";
                                }

                                if (!defined($sobject->{syncattrs})) {
                                    @sync_attrs = ($present_list->{$sbase}{$key}->{entryStr} =~ /^([^:]+):/gmi);
                                }

                                for (my $j = 0; $j < @sync_attrs; $j++) {
                                    my $attr = $sync_attrs[$j];
                                    my $sattr;

                                    if ($attr =~ /^(objectClass|$rdn_attr)$/i) {
                                        next;
                                    }

                                    if (defined($sobject->{syncattr})) {
                                        $sattr = $sobject->{syncattr}[$j];
                                    }

                                    my @values = $present_list->{$sbase}{$key}->{entryStr} =~ /^$attr: (.*)$/gmi;
                                    if (!@values) {
                                        next;
                                    }

                                    my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                    if (@sync_vals) {
                                        foreach my $value (@sync_vals) {
                                            $entryStr = "$entryStr$attr: $value\n";
                                        }
                                    }
                                }

                                $nosync_entry = encode('utf8', $nosync_entry);
                                $entryStr = encode('utf8', $entryStr);
                                $rc = $self->_doUpdate('add', undef, 1, $nosync_entry, $entryStr);
                                if ($rc) {
                                    $self->log(level => 'error', message => "Synchronizing \"$nosync_entry\" failed($rc)");
                                    return $rc;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    $self->_unlock($lock);

    return $rc;
}

sub _getPresentList
{
    my $self = shift;
    my $conf = $self->{_lism};
    my $sync = $conf->{sync}[0];
    my $present_list;

    foreach my $dname (keys %{$sync->{data}}) {
        my $sdata = $sync->{data}{$dname};

        foreach my $oname (keys %{$sdata->{object}}) {
            my $sobject = $sdata->{object}{$oname};

            foreach my $type ('syncdn', 'masterdn') {
                if (!defined($sobject->{$type})) {
                    next;
                }

                foreach my $typedn (@{$sobject->{$type}}) {
                    my $sbase = $typedn.','.$self->{data}{$lism_master}->{suffix};
                    if (defined($present_list->{$sbase})) {
                        next;
                    }

                    my ($rc, @entries) = $self->_do_search($sbase, 2, 0, $sizeLimit, 0, 'objectClass=*', 0, ());
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($key) = ($entries[$i] =~ /^dn: (.*),$sbase\n/i);
                        if (!$key) {
                            next;
                        }
                        $key = lc($key);

                        my $entryStr;
                        ($entryStr = $entries[$i]) =~ s/^dn:.*\n//;
		        $present_list->{$sbase}{$key}->{entryStr} = join("\n", sort split(/\n/, $entryStr));
                        $present_list->{$sbase}{$key}->{present} = 0;
                    }
                }
            }
        }
    }

    return $present_list;
}

sub _checkSyncAttrs
{
    my $self = shift;
    my ($src_data, $dst_data, $sattr, @values) = @_;
    my @sync_vals = ();

    foreach my $value (@values) {
        # check attribute synchronization rule
        if ($sattr && defined($sattr->{rule})) {
            my $doSyncAttr = 1;
            foreach my $rule (@{$sattr->{rule}}) {
                if ($value !~ /$rule/i) {
                    $doSyncAttr = 0;
                    last;
                }
            }
            if (!$doSyncAttr) {
                next;
            }
        }

        # replace suffix of dn values
        $value =~ s/$src_data->{suffix}$/$dst_data->{suffix}/i;

        # don't synchronize values of dn which isn't in this data directory
        if ($value =~ /$self->{_config}->{basedn}$/i &&
            $value !~ /$dst_data->{suffix}$/i) {
            next;
        }

        push(@sync_vals, $value);
    }

    return @sync_vals; 
}

sub _checkSyncedAttrs
{
    my $self = shift;
    my ($src_data, $dst_data, $sattr, @values) = @_;
    my @synced_vals = ();
    my @left_vals = ();

    foreach my $value (@values) {
        my $doSyncAttr = 1;

        # check attribute synchronization rule
        if ($sattr && defined($sattr->{rule})) {
            my $tmpval = $value;

            # replace suffix of dn values
            $tmpval =~ s/$src_data->{suffix}$/$dst_data->{suffix}/i;

            foreach my $rule (@{$sattr->{rule}}) {
                if ($tmpval !~ /$rule/i) {
                    $doSyncAttr = 0;
                    last;
                }
            }
        }

        if ($doSyncAttr) {
            push(@synced_vals, $value);
        } else {
            push(@left_vals, $value);
        }
    }

    return (\@synced_vals, \@left_vals);
}

sub _cmpPwdHash
{
    my $self = shift;
    my ($val1, $val2) = @_;

    my ($htype1) = ($val1 =~ /^\{([^\}]+)\}/);
    my ($htype2) = ($val2 =~ /^\{([^\}]+)\}/);

    if ($htype1 ne $htype2) {
        return 0;
    } else {
        return 1;
    }
}

sub _unique
{
    my $self = shift;
    my @array = @_;

    my %hash = map {$_ => 1} @array;

    return keys %hash;
}

sub _getDataName
{
    my $self = shift;
    my ($dn) = @_;

    foreach my $dname (keys %{$self->{data}}) {
        if ($dn =~ /$self->{data}{$dname}->{suffix}$/i) {
            return $dname;
        }
    }

    return undef;
}

sub _getStorage
{
    my $self = shift;
    my ($dname) = @_;

    if (defined($self->{_storage}{$dname})) {
        return $self->{_storage}{$dname};
    } else {
        return undef;
    }
}

=pod

=head2 _lock($lock, $flag)

get global lock for all data.

=cut

sub _lock
{
    my $self = shift;
    my ($lock, $flag) = @_;
    my $fd;

    if (!open($fd, "> $lock")) {
        return undef;
    }
    flock($fd, $flag);

    return $fd;
}

=head2 _unlock($fd)

release global lock for all data.

=cut

sub _unlock
{
    my $self = shift;
    my ($fd) = @_;

    close($fd);
}

sub _writeSyncFail
{
    my $self = shift;
    my ($func, $dname, $dn, @info) = @_;
    my $conf = $self->{_config};
    my $fd;
    my $ldif;

    if (!defined($conf->{syncdir})) {
        return 0;
    }

    if (!open($fd, ">> $conf->{syncdir}/$syncFailLog-$dname.log")) {
        $self->log(level => 'crit', message => "Can't open synchronization failure log: $conf->{syncdir}/$syncFailLog-$dname.log");
        return -1;
    }

    flock($fd, 2);
    $ldif = "# ".strftime("%Y%m%d%H%M%S", localtime(time))."\ndn: $dn\nchangetype: $func\n";

    if ($func eq 'modify') {
        while (@info > 0) {
            my $action = shift @info;
            my $attr = shift @info;
            my @values;

            while (@info > 0 && $info[0] ne "ADD" && $info[0] ne "DELETE" && $info[0] ne "REPLACE") {
                push(@values, shift @info);
            }

            $ldif = $ldif.lc($action).": $attr\n";
            foreach my $val (@values) {
                $ldif = "$ldif$attr: $val\n";
            }
            $ldif = "$ldif-\n";
        }
    } elsif ($func eq 'add') {
        $ldif = "$ldif$info[0]"
    } elsif ($func eq 'modrdn') {
        $ldif = $ldif."newrdn: $info[0]\ndeleteoldrdn: $info[1]\n";
    }

    print $fd "$ldif\n";

    close($fd);

    return 0;
}

sub _auditMsg
{
    my $self = shift;
    my ($type, $dn, $result, @info) = @_;
    my $message = '';

    if ($type eq 'modify') {
        while (@info > 0) {
            my $action = shift @info;
            my $attr = shift @info;
            my @values;
            my $dsn;

            while (@info > 0 && $info[0] ne "ADD" && $info[0] ne "DELETE" && $info[0] ne "REPLACE") {
                push(@values, shift @info);
            }

            if ($action eq "ADD") {
                $dsn = '+';
            } elsif ($action eq "DELETE") {
                $dsn = '-';
            } else {
                $dsn = '=';
            }

            my $mod;
            if ($attr =~ /^userPassword$/i) {
                $mod = "$attr:$dsn";
            } else {
                $mod = "$attr:$dsn".join(';', @values);
            }

            if ($message) {
                $message = "$message $mod";
            } else {
                $message = $mod;
            }
        }
    } elsif ($type eq 'add') {
        my @list = split("\n", $info[0]);
        my $prev;

        while (@list > 0) {
            my $elt = shift @list;
            my ($attr, $value) = ($elt =~ /^([^:]*): (.*)$/);

            my $mod;
            if ($attr =~ /^userPassword$/i) {
                $mod = "$attr:+";
            } else {
                $mod = "$attr:+$value";
            }

            if ($prev eq $attr) {
                $message = "$message;$value";
            } elsif ($message) {
                $message = "$message $mod";
            } else {
                $message = $mod;
            }

            $prev = $attr;
        }
    } elsif ($type eq 'modrdn') {
        $message = "newrdn=$info[0]";
    }

    return "type=$type dn=\"$dn\" result=$result $message";
}

=head1 SEE ALSO

slapd(8), slapd-perl(5)

=head1 AUTHOR

Kaoru Sekiguchi, <sekiguchi.kaoru@secioss.co.jp>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2006 by Kaoru Sekiguchi

This library is free software; you can redistribute it and/or modify
it under the GNU LGPL.

=cut

1;
