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 LISM::Storage;
use Data::Dumper;
if ($^O ne 'MSWin32') {
    eval "use Sys::Syslog";
} else {
    eval "use Log::Dispatch::FileRotate";
}

our $VERSION = '2.3.12';

our $lism_master = 'lism_master';
our $syncrdn = 'cn=sync';
our $master_syncrdn = 'cn=master-sync';
our $cluster_syncrdn = 'cn=cluster-sync';
our $syncInfoEntry = "objectClass: lismSync\n";
our $syncInfoAttr = "lismSyncStatus";
our $nosyncAttr = "lismSyncErrMessage";
our $syncDataAttr = "lismSyncErrNode";
our $syncFilterAttr = "lismSyncFilter";
our $syncBaseAttr = "lismSyncBase";
our $syncSizeAttr = "lismSyncSizeLimit";
our $clusterrdn = 'cn=cluster';
our $clusterEntry = "objectClass: lismCluster\n";
our $masterAttr = "lismClusterMaster";
our $clusterAttr = "lismClusterNode";
our $activeAttr = "lismClusterActive";
our $optionAttr = "lismCmdOption";
our $confrdn = 'cn=config';
our $confOpAttr = "lismConfigOperation";
our $authzdn = 'cn=authz';
our $syncFailLog = 'syncfail';
our $sizeLimit = 1000000;
our $lockFile = 'lism.lock';

=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 ( @v > 1 ) {
        $self->{_config}->{$k} = \@v;
    } else {
        if ($k eq 'admindn' || $k eq 'basedn') {
            ($self->{_config}->{$k} = $v[0]) =~ tr/A-Z/a-z/;
        } 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};

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

    $self->log(level => 'info', message => "LISM $VERSION starting");

    return $self->_startup();
}

=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, $ip) = @_;
    my $conf = $self->{_lism};
    my $timeout = $self->{_config}->{timeout};
    my $dn;
    my $filterStr;
    my $rc = LDAP_NO_SUCH_OBJECT;

    if (!$binddn) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    $self->_lock(1);

    DO: {
        # decode bind dn
        $binddn = decode('utf8', $binddn);

        $binddn =~ s/,\s+/,/;

        if ($binddn =~ /$authzdn,$self->{_config}->{basedn}$/i) {
            ($binddn, $dn) = ($binddn =~ /^authzFrom=(.+),authzTo=(.+),$authzdn,$self->{_config}->{basedn}$/i);
            $binddn =~ s/\\3D/=/g;
            $dn =~ s/\\3D/=/g;
            if ($binddn =~ /^$self->{_config}->{admindn}$/i) {
                $filterStr = '(objectClass=*)';
            } else {
                $filterStr = "(authzFrom=$binddn)";
            }
        }
        $dn = $dn ? $dn : $binddn;
        $filterStr = $filterStr ? $filterStr : '(objectClass=*)';

        # check bind by administration user
        if ($binddn =~ /^$self->{_config}->{admindn}$/i) {
            if (defined($self->{_config}->{adminpw}) && $passwd eq $self->{_config}->{adminpw}) {
                $rc = LDAP_SUCCESS;
                if ($binddn eq $dn) {
                    $self->{bind}{dn} = $dn;
                    if ($ip) {
                        $self->{bind}{ip} = $ip;
                    }
                    last DO;
                }
            } else {
                $rc = LDAP_INVALID_CREDENTIALS;
                last DO;
            }
        } else {
            my $dname = $self->_getDataName($binddn);
            if (!$dname) {
                $rc = LDAP_INVALID_CREDENTIALS;
                last DO;
            }

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

            # 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);
                }
            } else {
                $rc = LDAP_INVALID_CREDENTIALS;
                last DO;
            }

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

        if (!$rc) {
            # set binddn
            my @entries = ();
            $self->_unlock();
            ($rc, @entries) = $self->_do_search($dn, 0, 0, 1, $timeout, $filterStr, 0);
            $self->_lock(1);
            if (!$rc && @entries) {
                $self->{bind}{dn} = $self->_replMasterDn(($entries[0] =~ /^dn: (.*)\n/)[0]);
                $self->{bind}{entryStr} = $self->_replMasterDn($entries[0]);
                if ($ip) {
                    $self->{bind}{ip} = $ip;
                }
            } elsif ($rc == LDAP_NO_SUCH_OBJECT || !@entries) {
                $rc = LDAP_INVALID_CREDENTIALS;
            } else {
                $rc = LDAP_OTHER;
            }
        }
    }

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

    $self->_unlock();

    $self->auditlog('bind', $binddn, $rc);

    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};
    my @match_entries = ();

    if (!$base) {
        return (LDAP_UNWILLING_TO_PERFORM, @match_entries);
    }

    # set size limit
    $sizeLim = $sizeLim < 0 ? $sizeLimit : $sizeLim;

    # set timeout
    $timeLim = $timeLim > 0 ? $timeLim : $self->{_config}->{timeout};

    # decode base
    $base = decode('utf8', $base);

    # get cluster information
    if ($base =~ /^$clusterrdn,$self->{_config}->{basedn}$/i) {
        if (!$self->_accessAllowed($base, 'read')) {
            return(LDAP_INSUFFICIENT_ACCESS, @match_entries);
        }

        return $self->_getClusterInfo($base, $scope, $filterStr, $attrOnly, @attrs);
    }

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

    my ($rc, @entries) = $self->_do_search($base, $scope, $deref, $sizeLim, $timeLim, $filterStr, $attrOnly, @attrs);

    foreach my $entry (@entries) {
        if ($self->_accessAllowed(($entry =~ /^dn: (.*)\n/)[0], 'read')) {
            push(@match_entries, $entry);
        }
    }

    return ($rc, @match_entries);
}

=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;

    # decode dn, value
    $dn = decode('utf8', $dn);
    $avaStr = decode('utf8', $avaStr);

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

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

    if (!$self->_accessAllowed($dn, 'read')) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    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 @pwd_mod_list;
    my $rc;

    # decode dn, values
    $dn = decode('utf8', $dn);
    for (my $i = 0; $i < @list; $i++) {
        $list[$i] = decode('utf8', $list[$i]);
        if ($list[$i] =~ /^userpassword$/i && $list[$i + 1] && $list[$i + 1] !~ /^{[^}]+}/) {
            # add plain text password
            push(@pwd_mod_list, ($list[$i - 1], 'plainpassword', $list[$i + 1 ]));
        }
    }
    if (@pwd_mod_list) {
        push(@list, @pwd_mod_list);
    }

    $dn =~ tr/A-Z/a-z/;
    $dn =~ s/,\s+/,/g;

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

    if (!$self->_accessAllowed($dn, 'write')) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    # reload configuration
    if ($dn =~ /^$confrdn,$self->{_config}{basedn}$/i) {
        return $self->_setConfig($dn, @list);
    }

    # set cluster information
    if ($dn =~ /^$clusterrdn,$self->{_config}{basedn}$/i) {
        return $self->_setClusterInfo($dn, @list);
    }

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

    $rc = $self->_doUpdate('modify', undef, 1, $dn, @list);

    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+//g;
    my ($dn) = ($entryStr =~ /^dn:{1,2} (.*)$/m);
    if ($entryStr =~ /^dn::/) {
        $dn = decode_base64($dn);
    }

    # decode dn, entry
    $dn = decode('utf8', $dn);
    $entryStr = decode('utf8', $entryStr);

    $entryStr =~ s/^dn:.*\n//;
    $dn =~ tr/A-Z/a-z/;
    $dn =~ s/,\s+/,/g;

    # decode base64
    $entryStr = $self->_decBase64Entry($entryStr);

    if ($entryStr =~ /^userpassword:\s+([^\s]+)$/mi) {
        my $plainpw = $1;
        if ($plainpw !~ /^{[^}]+}/) {
            $entryStr =~ s/^userpassword:.*$/userpassword: $plainpw\nplainpassword: $plainpw/mi;
        }
    }

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

    if (!$self->_accessAllowed($dn, 'write')) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    $rc = $self->_doUpdate('add', undef, 1, $dn, $entryStr);

    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;

    # decode dn, rdn
    $dn = decode('utf8', $dn);
    $newrdn = decode('utf8', $newrdn);

    $dn =~ tr/A-Z/a-z/;
    $dn =~ s/,\s+/,/g;

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

    if (!$self->_accessAllowed($dn, 'write')) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    $rc = $self->_doUpdate('modrdn', undef, 1, $dn, $newrdn, $delFlag);

    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;

    # decode dn
    $dn = decode('utf8', $dn);

    $dn =~ tr/A-Z/a-z/;
    $dn =~ s/,\s+/,/g;

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

    if (!$self->_accessAllowed($dn, 'write')) {
        return LDAP_INSUFFICIENT_ACCESS;
    }

    $rc = $self->_doUpdate('delete', undef, 1, $dn);

    return $rc;
}

=pod

=head2 log(level, message)

log message to syslog.

=cut

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

    if (Encode::is_utf8($p{'message'})) {
        $p{'message'} = encode('utf8', $p{'message'});
    }
    $p{'message'} = uc($p{'level'}).' ['.ref($self).'] '.$p{'message'};

    if ($^O ne 'MSWin32') {
        openlog('LISM', 'pid', $conf->{syslogfacility});
        setlogmask(Sys::Syslog::LOG_UPTO(Sys::Syslog::xlate($conf->{sysloglevel})));
        syslog($p{'level'}, sprintf("%.512s", $p{'message'}));
        closelog();
    } else {
        $self->{log}->log(level => $p{'level'}, message => strftime("%Y/%m/%d %H:%M:%S", localtime(time))." ".$p{'message'}."\n");
    }

    if (defined($conf->{printlog}) && $conf->{printlog} =~ /$p{'level'}/) {
        print $p{'message'}."\n";
    }
}

=pod

=head2 auditlog($type, $dn, $result, @info)

write to the audit log.

=cut

sub auditlog
{
    my $self = shift;
    my $conf = $self->{_config};

    my $message = $self->_auditMsg(@_);
    if (!$message) {
        return;
    }

    if ($^O ne 'MSWin32') {
        openlog('LISM', 'pid', $conf->{auditfacility});
        syslog('info', $message);
        closelog();
    } else {
        $self->{audit}->log(level => 'info', message => strftime("%Y/%m/%d %H:%M:%S", localtime(time))." $message\n");
    }

    if (defined($conf->{'printlog'}) && $conf->{'printlog'} =~ /audit/) {
        print "$message\n";
    }
}

=pod

=head2 _slapdCofig()

check slapd configuration.

=cut

sub _slapdConfig
{
    my $self = shift;
    my $conf = $self->{_config};

    if (!defined($conf->{sysloglevel})) {
        $conf->{sysloglevel} = 'info';
    }

    if ($^O ne 'MSWin32') {
        if (!defined($conf->{syslogfacility})) {
            $conf->{syslogfacility} = 'local4';
        }
        if (!defined($conf->{auditfacility})) {
            $conf->{auditfacility} = 'local4';
        }
    } else {
        if (!defined($conf->{logfile})) {
            return 1;
        }

        my $timezone = 'JST';
        my $rotatedate = 'yyyy-MM-dd';
        my $rotatenum = 4;
        if (defined($conf->{logtimezone})) {
            $timezone = $conf->{timezone};
        }
        if (defined($conf->{logrotatedate})) {
            $rotatedate = $conf->{logrotatedate};
        }
        if (defined($conf->{logrotatenum})) {
            $rotatenum = $conf->{logrotatenum};
        }
        $self->{log} = Log::Dispatch::FileRotate->new(name => 'LISM',
                                                     min_level => $conf->{sysloglevel},
                                                     filename => $conf->{logfile},
                                                     mode => 'append',
                                                     TZ => $timezone,
                                                     DatePattern => $rotatedate,
                                                     max => $rotatenum);
        if (defined($conf->{auditfile})) {
            $self->{audit} = Log::Dispatch::FileRotate->new(name => 'LISM',
                                                     min_level => 'info',
                                                     filename => $conf->{auditfile},
                                                     mode => 'append',
                                                     TZ => $timezone,
                                                     DatePattern => $rotatedate,
                                                     max => $rotatenum);
        } else {
            $self->{audit} = $self->{log};
        }
    }

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

    return 0;
}

=pod

=head2 _lismCofig()

check lism configuration.

=cut

sub _lismConfig
{
    my $self = shift;
    my $conf = $self->{_config};

    # parse XML configuration
    $self->{_lism} = XMLin($conf->{conf}, ForceArray => 1);
    my $lismconf = $self->{_lism};

    if (!defined($self->{data})) {$self->{data} = {}}
    foreach my $dname (keys %{$lismconf->{data}}) {
        my $dconf = $lismconf->{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
        $dconf->{container}[0]->{rdn}[0] =~ tr/A-Z/a-z/;
        $self->{data}{$dname}->{suffix} = $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;

        # check access rule
        if (defined($dconf->{access})) {
            foreach my $access (@{$dconf->{access}}) {
                if (!defined($access->{dn})) {
                    $self->log(level => 'alert', message => "access rule doesn't have dn");
                    return 1;
                }

                foreach my $right (@{$access->{right}}) {
                    if ($right->{op} ne 'read' && $right->{op} ne 'write') {
                        $self->log(level => 'alert', message => "access operation must read or write");
                        return 1;
                    }

                    if (defined($right->{type}) &&
                        $right->{type}[0] !~ /^(\*|self)$/) {
                        $self->log(level => 'alert', message => "access type is invalid");
                        return 1;
                    }
                }
            }
        }
    }

    if ((!defined($conf->{disable}) || $conf->{disable} ne 'sync') && defined($lismconf->{sync})) {
        my $sync = $lismconf->{sync}[0];

        # set cluster
        $self->{cluster} = {};

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

            $self->{master} = {};
            $self->{master}->{primary} = $self->{master}->{current} = $sync->{master}[0]->{data}[0];
            $self->_initMaster($sync->{master}[0]->{data}[0]);

            if (defined($sync->{master}[0]->{backup})) {
                $self->{master}->{backup} = $sync->{master}[0]->{backup};
            } else {
                $self->{master}->{backup} = ();
            }
        }

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

            if (!defined($lismconf->{data}{$dname})) {
                $self->log(level => 'alert', message => "Data $dname for 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;
            my %clustertype;
            foreach my $oname (keys %{$sdata->{object}}) {
                my $sobject = $sdata->{object}{$oname};

                # ignore dn
                if (!defined($sobject->{dnignore})) {
                    $sobject->{dnignore}[0] = 'off';
                }

                # unique entry
                if (defined($sobject->{unique})) {
                    if (!defined($sobject->{unique}[0]->{base})) {
                        $self->log(level => 'alert', message => "unique must have base");
                        return 1;
                    }
                    if ($sobject->{dnignore}[0] eq 'on') {
                        $self->log(level => 'alert', message => "unique must not be set with dnignore");
                        return 1;
                    }
                }

                # nomalize dn
                if (defined($sobject->{syncdn})) {
                    if ($sobject->{dnignore}[0] eq 'on' && !defined($sobject->{syncfilter})) {
                        $self->log(level => 'alert', message => "syncfilter must be set if dnignore is on");
                        return 1;
                    }

                    for (my $i = 0; $i < @{$sobject->{syncdn}}; $i++) {
                        $sobject->{syncdn}[$i] =~ tr/A-Z/a-z/;
                    }

                    $clustertype{'cluster'} = 1;
                }

                if (defined($sobject->{masterdn})) {
                    if ($sobject->{dnignore}[0] eq 'on' && !defined($sobject->{masterfilter})) {
                        $self->log(level => 'alert', message => "masterfilter must be set if dnignore is on");
                        return 1;
                    }

                    for (my $i = 0; $i < @{$sobject->{masterdn}}; $i++) {
                        $sobject->{masterdn}[$i] =~ tr/A-Z/a-z/;
                    }

                    $clustertype{'master'} = 1;
                }

                if (defined($sobject->{syncflag}) && ref $sobject->{syncflag}[0]) {
                    if (!defined($sobject->{syncflag}[0]->{match}) ||
                        !defined($sobject->{syncflag}[0]->{dn}) ||
                        !defined($sobject->{syncflag}[0]->{filter})) {
                        $self->log(level => 'alert', message => "invalid attributes of syncflag");
                        return 1;
                    }
                    $sobject->{syncflag}[0]->{filter} =~ s/&amp;/&/g;
                }

                if (defined($sobject->{syncfilter})) {
                    $sobject->{syncfilterobj} = Net::LDAP::Filter->new($sobject->{syncfilter}[0]);
                }

                if (defined($sobject->{masterfilter})) {
                    $sobject->{masterfilterobj} = Net::LDAP::Filter->new($sobject->{masterfilter}[0]);
                }

                # 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;
                        }
                        if (defined($attr->{filter})) {
                            $attr->{filterobj} = Net::LDAP::Filter->new($attr->{filter}[0]);
                        }
                        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;
                        }
                        if (defined($attr->{filter})) {
                            $attr->{filterobj} = Net::LDAP::Filter->new($attr->{filter}[0]);
                        }
                        push(@{$sobject->{masterattrs}}, $attr->{name}[0]);
                    }
                }
            }

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

            # set cluster
            $self->{cluster}{$dname}->{conf} = $sdata;
            $self->{cluster}{$dname}->{status} = 'active';
            $self->{cluster}{$dname}->{info} = join(',', keys %clustertype);
        }

        $self->{cluster}->{$self->{master}->{primary}}->{status} = 'active';
        $self->{cluster}->{$self->{master}->{primary}}->{info} = 'lism-master';
        if ($self->{master}->{backup}) {
            $self->{cluster}->{$self->{master}->{primary}}->{conf} = $self->{cluster}{$self->{master}->{backup}[0]}->{conf};
        } else {
            $self->{cluster}->{$self->{master}->{primary}}->{conf} = undef;
        }
    }

    return 0;
}

sub _initMaster
{
    my $self = shift;
    my ($dname) = @_;
    my $conf = $self->{_lism};
    my $sync = $conf->{sync}[0];

    undef($self->{data}{$lism_master});
    $self->{data}{$lism_master} = {};

    if (!$dname) {
        return 0;
    }

    my $master = $self->{data}{$lism_master};
    my $src_data = $self->{data}{$dname};

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

    $self->_initData($lism_master);

    return 0;
}

sub _startup
{
    my $self = shift;
    my $conf;

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

    foreach my $dname (keys %{$self->{data}}) {
        if ($dname ne $lism_master && $self->_initData($dname)) {
            return 1;
        }
    }

    return 0;
}

sub _destroy
{
    my $self = shift;

    undef($self->{_storage});

    undef($self->{_handler});

    undef($self->{data});

    undef($self->{_lism});

    undef($self->{cluster});

    undef($self->{master});

    return 0;
}

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

    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}->{lism} = $self;

        $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};
    $self->{_storage}{$dname}->{lism} = $self;

    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;
    }

    if (defined($dconf->{status})  && $dconf->{status}[0] eq 'disable') {
        $self->_removeCluster($dname);
    }

    return 0;
}

=head2 _lock($flag)

get global lock for internal data.

=cut

sub _lock
{
    my $self = shift;
    my ($flag) = @_;
    my $conf = $self->{_config};

    if (!open($self->{lock}, "> $conf->{syncdir}/$lockFile")) {
        return 1;
    }

    flock($self->{lock}, $flag);

    return 0;
}

=head2 _unlock()

release global lock for internal data.

=cut

sub _unlock
{
    my $self = shift;

    close($self->{lock});

    return 0;
}

sub _accessAllowed
{
    my $self = shift;
    my ($dn, $op) = @_;
    my $rc = 0;

    CHECK: {
        if ($self->{bind}{dn} =~ /^$self->{_config}->{admindn}$/i) {
            $rc = 1;
            last CHECK;
        }

        my $dname = $self->_getDataName($dn);
        if (!$dname) {
            $rc = 1;
            last CHECK;
        }

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

        # change dn to original data if dn belongs to master data
        $dn = $self->_replMasterDn($dn);

        if (defined($dconf->{access})) {
            foreach my $access (@{$dconf->{access}}) {
                if ($dn !~ /$access->{dn}/i) {
                    next;
                }

                my @matches = ($dn =~ /$access->{dn}/i);

                foreach my $right (@{$access->{right}}) {
                    if ($op eq 'write' && $right->{op} ne 'write') {
                        next;
                    }

                    if (defined($right->{type})) {
                        my $type = $right->{type}[0];
                        if ($type eq '*') {
                            $rc = 1;
                            last CHECK;
                        } elsif ($type eq 'self') {
                            if ($self->{bind}{dn} =~ /^$dn$/i) {
                                $rc = 1;
                                last CHECK;
                            }
                        }
                    }

                    if (defined($right->{dn})) {
                        foreach my $allowdn (@{$right->{dn}}) {
                            my $tmpdn = $allowdn;
                            for (my $i = 0; $i < @matches; $i++) {
                                my $num = $i + 1;
                                $tmpdn =~ s/%$num/$matches[$i]/;
                            }
                            if ($self->{bind}{dn} =~ /$tmpdn/i) {
                                $rc = 1;
                                last CHECK;
                            }
                        }
                    }

                    if (defined($right->{filter})) {
                        my $filterStr = $right->{filter}[0];
                        for (my $i = 0; $i < @matches; $i++) {
                            my $num = $i + 1;
                            $filterStr =~ s/%$num/$matches[$i]/;
                        }

                        my $filter = Net::LDAP::Filter->new($filterStr);
                        if (!defined($filter)) {
                            next;
                        }

                        if (LISM::Storage->parseFilter($filter, $self->{bind}{entryStr})) {
                            $rc = 1;
                            last CHECK;
                        }
                    }
                }
                last CHECK;
            }
            $rc = 1;
        } else {
            $rc = 1;
        }
    }

    return $rc;
}

sub _replMasterDn
{
    my $self = shift;
    my ($str, $orgstr) = @_;

    if (!defined($self->{data}{$lism_master}->{suffix})) {
        return $str;
    }

    my $master_suffix = $self->{data}{$lism_master}->{suffix};
    my $current_suffix = $self->{data}{$self->{master}->{current}}->{suffix};

    if ($orgstr) {
        if ($orgstr =~ /$master_suffix$/mi) {
            $str =~ s/$current_suffix/$master_suffix/gmi;
        }
    } else {
        $str =~ s/$master_suffix$/$current_suffix/gmi;
    }

    return $str;
}

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 = ();
    my $sync = 0;
    if ($deref == 5) {
        $sync = 1;
        $deref = 0;
    }

    if ($base =~ /^$self->{_config}->{basedn}$/i) {
        if ($scope != 0) {
            # scope isn't base
            foreach my $dname (keys %{$self->{data}}) {
                if (defined($self->{data}{$dname}->{suffix})) {
                    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;
    }

    if ($self->{_config}->{sysloglevel} eq 'debug') {
        $self->log(level => 'debug', message => "type=search base=$base scope=$scope deref=$deref filter=$filterStr");
    }

    foreach my $srchbase (@srchbases) {
        my $dfilterStr = $filterStr;
        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, \$dfilterStr);
        if ($rc) {
            last;
        }

        # delete values of dn which isn't in this data directory
        if (!$storage->manageDIT() && defined($self->{data}{$lism_master}->{suffix})) {
            $dfilterStr =~ s/$self->{data}{$lism_master}->{suffix}\)/$self->{data}{$dname}->{suffix})/i;
            my @elts = ($dfilterStr =~ /(\([^(]+,$self->{_config}->{basedn}\))/gi);
            for (my $i = 0; $i < @elts; $i++) {
                if ($elts[$i] !~ /$self->{data}{$dname}->{suffix}\)$/i) {
                    $elts[$i] =~ s/([.*+?\[\]()|\^\$\\])/\\$1/g;
                    $dfilterStr =~ s/$elts[$i]/(objectClass=*)/;
                }
            }
        }

        # call search of the appropriate storage
        ($rc, @subentries) = $storage->search($srchbase, $scope, $deref, $sizeLim, $timeLim, $dfilterStr, $attrOnly, @attrs);
        if ($rc == LDAP_SERVER_DOWN) {
            $self->log(level => 'err', message => "Searching in $dname failed($rc)");
            if ($base =~ /^$self->{_config}->{basedn}$/i) {
                $rc = LDAP_SUCCESS;
                next;
            } else {
                last;
            }
        } elsif ($rc) {
            $self->log(level => 'err', message => "Searching in $dname failed($rc)");
            last;
        }

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

        if ($self->{_config}->{sysloglevel} eq 'debug') {
            for (my $i = 0; $i < @subentries; $i++) {
                $self->log(level => 'debug', message => "search result: \"$subentries[$i]\"");
            }
        }

        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}$/i) {
        # 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;
    }

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

    if (!$rc) {
        # replicate the udpate operation to the storages
        if ($self->_checkSync($dname) && $commit) {
            ($rc, @updated) = $self->_doSync($func, $src_data, $dn, @info);
        } else {
            $self->log(level => 'debug', message => "$func: \"$dn\n".join("\n", @info)."\"");
            if (!($rc = $self->$method($dname, $dn, @info))) {
                push(@updated, $dname);
            }
        }
    }

    if (!$self->_checkSync($dname) || $commit) {
        # do post handler
        if ($rc) {
            $self->_doHandler('unlock', $func, $dname, \$dn, \@info, \$oldentry);
        } else {
            $rc = $self->_doHandler('post', $func, $dname, \$dn, \@info, \$oldentry);
            if ($rc == LDAP_USER_CANCELED) {
                push(@updated, $dname);
            }
        }
    }

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

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

    if (!$self->_checkSync($dname) || $commit) {
        $self->auditlog($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) {
            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") {
                my $value = shift @list;
                if ($storage->manageDIT() ||
                    $value !~ /$self->{_config}->{basedn}$/i ||
                        $value =~ /$self->{data}{$dname}->{suffix}/i) {
                    push(@values, $value);
                }
            }

            if ($key =~ /^lismparentdn$/i) {
                if ($action eq "REPLACE" && @values && $values[0]) {
                    if ($dn =~ /^[^,]+,$values[0]$/i) {
                        return LDAP_SUCCESS;
                    } else {
                        return $storage->move($dn, $values[0]);
                    }
                } else {
                    return LDAP_UNWILLING_TO_PERFORM;
                }
            }

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

                $values[0] = $hashpw;
            }

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

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

    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+([^\s]+)$/mi) {
            my $plainpw = $1;
            my $hashpw = $storage->hashPasswd($plainpw);
            if (defined($hashpw)) {
                $entryStr =~ s/^userpassword:.*$/userpassword: $hashpw/mi;
            } else {
                $entryStr =~ s/\nuserpassword:.*\n/\n/i;
            }
        }

        if (!$storage->manageDIT()) {
            my @dn_vals = ($entryStr =~ /^(.+$self->{_config}->{basedn})$/gmi);
            if (@dn_vals) {
                foreach my $value (@dn_vals) {
                    if ($value !~ /$self->{data}{$dname}->{suffix}$/i) {
                        $entryStr =~ s/\n$value\n/\n/i;
                    }
                }
            }
        }

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

    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);
    } else {
        $rc = LDAP_UNAVAILABLE;
    }

    return $rc;
}

sub _doHandler
{
    my $self = shift;
    my ($type, $func, $dname, @args) = @_;
    my $method = $type.'_'.$func;
    my $timeout = $self->{_config}->{timeout};
    my $dn;
    my $rc = LDAP_SUCCESS;
    my (@orders) = ('first', 'middle', 'last');
    my $update = ($func =~ /^(add|modify|delete)$/);

    if ($self->{operation} eq 'sync' || $update) {
        push(@orders, 'sync');
    }

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

    if ($func ne 'search') {
        # change dn to orignal data if dn belongs to master data
        $dn = ${$args[0]};
        ${$args[0]} = $self->_replMasterDn(${$args[0]});
    }

    foreach my $order (@orders) {
        foreach my $hname (keys %{$self->{_handler}{$dname}}) {
            if ($self->{_handler}{$dname}{$hname}->getOrder() ne $order) {
                next;
            }

            if ($type eq 'pre' && $func =~ /^(modify|delete)$/ &&
                $self->{_handler}{$dname}{$hname}->useprevious() && !defined(${$args[2]})) {
                ($rc, ${$args[2]}) = $self->_do_search(${$args[0]}, 0, 0, 1, $timeout, '(objectClass=*)', 0);
                if ($rc) {
                    $self->log(level => 'err', message => "Saving ${$args[0]} for handler failed($rc)");
                    last;
                }
            }

            if ($type eq 'pre' && $update) {
                if ($self->{_handler}{$dname}{$hname}->lock()) {
                    $rc = LDAP_OTHER;
                    last;
                }
            }

            if ($type ne 'unlock') {
                $rc = $self->{_handler}{$dname}{$hname}->$method(@args);
            }
            if (($type eq 'post' || $type eq 'unlock') && $func =~ /^(add|modify|delete)$/) {
                $self->{_handler}{$dname}{$hname}->unlock();
            }

            if ($rc) {
                last;
            }
        }
        if ($rc) {
            last;
        }
    }

    if ($func ne 'search') {
        ${$args[0]} = $self->_replMasterDn(${$args[0]}, $dn);
    }

    return $rc;
}

sub _setConfig
{
    my $self = shift;
    my ($dn, @list) = @_;

    my $modinfo = join(',', @list);
    if ($modinfo !~ /REPLACE,$confOpAttr,reload/i) {
        return LDAP_UNWILLING_TO_PERFORM;
    }

    $self->_lock(2);

    $self->_destroy();

    if ($self->_startup()) {
        $self->log(level => 'alert', message => "Reload configuration failed");
        exit 1;
    }

    $self->_unlock();

    return 0;
}

sub _addCluster
{
    my $self = shift;
    my ($dname, $nosync) = @_;
    my $cluster = $self->{cluster};
    my $rc;

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

    $self->_lock(2);

    if ($cluster->{$dname}->{status} eq 'inactive') {
        $cluster->{$dname}->{status} = 'busy';
        $self->_unlock();

        if (!$nosync) {
            $rc = $self->_setSyncInfo("$cluster_syncrdn,".$self->{_config}->{basedn}, ('DELETE', $syncDataAttr, $dname));
        }

        $self->_lock(2);
        if ($rc) {
            $self->log(level => 'err', message => "Adding \"$dname\" to cluster failed($rc)");
            $cluster->{$dname}->{status} = 'inactive';
        } else {
            if (defined($self->{master})) {
                if ($dname eq $self->{master}->{primary}) {
                    $self->_failback();
                }
            }
            $cluster->{$dname}->{status} = 'active';
        }
    }

    $self->_unlock();

    $self->log(level => 'info', message => "Add cluster \"$dname\"");

    return $rc;
}

sub _removeCluster
{
    my $self = shift;
    my ($dname) = @_;
    my $cluster = $self->{cluster};
    my $master = $self->{master};

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

    $self->_lock(2);

    if ($cluster->{$dname}->{status} eq 'active') {
        $cluster->{$dname}->{status} = 'inactive';

        if (defined($self->{master})) {
            if ($dname eq $master->{current}) {
                $self->_failover();
            }
        }
    }

    $self->log(level => 'err', message => "Remove cluster \"$dname\"");

    $self->_unlock();

    return 0;
}

sub _failover
{
    my $self = shift;
    my $cluster = $self->{cluster};
    my $master = $self->{master};

    $master->{current} = '';

    foreach my $backup (@{$master->{backup}}) {
        if ($cluster->{$backup}->{status} eq 'active') {
            $master->{current} = $backup;
            last;
        }
    }

    $self->_initMaster($master->{current});

    return 0;
}

sub _failback
{
    my $self = shift;
    my $cluster = $self->{cluster};
    my $master = $self->{master};

    $master->{current} = $master->{primary};

    $self->_initMaster($master->{current});

    return 0;
}

sub _getClusterInfo
{
    my $self = shift;
    my ($base, $scope, $filterStr, $attrOnly, @attrs) = @_;
    my $cluster = $self->{cluster};

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

    my $clusterentry = "dn: $base\n$clusterEntry";
    if ($self->{master}->{current}) {
        $clusterentry = "$clusterentry$masterAttr: $self->{master}->{current}\n";
    }

    foreach my $dname (keys %{$cluster}) {
        $clusterentry = "$clusterentry$clusterAttr: $dname\n";
    }
    foreach my $dname (keys %{$cluster}) {
        if ($dname ne $lism_master && $cluster->{$dname}->{status} eq 'active') {
            $clusterentry = "$clusterentry$activeAttr: $dname#$cluster->{$dname}->{info}\n";
        }
    }

    return (LDAP_SUCCESS, ($clusterentry));
}

sub _setClusterInfo
{
    my $self = shift;
    my ($dn, @list) = @_;
    my $cluster = $self->{cluster};
    my $rc = LDAP_SUCCESS;

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

    # get synchronized data
    my ($add_dnames) = ($modinfo =~ /ADD,$activeAttr,(.*),?(ADD|DELETE|REPLACE|)/i);
    my ($delete_dnames) = ($modinfo =~ /DELETE,$activeAttr,(.*),?(ADD|DELETE|REPLACE|)/i);

    if ($delete_dnames) {
        foreach my $dname (keys %{$cluster}) {
            if (",$delete_dnames," =~ /$dname,/i) {
                $self->_removeCluster($dname);
            }
        }
    }

    if ($add_dnames) {
        foreach my $dname (keys %{$cluster}) {
            if ($dname eq $lism_master) {
                next;
            }

            if (",$add_dnames," =~ /$dname,/i) {
                my $nosync = 0;
                if ($modinfo =~ /,$optionAttr,nosync(,|)/i) {
                    $nosync = 1;
                }
                $self->_addCluster($dname, $nosync);
            }
        }
    }

    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 $master = $self->{data}{$lism_master};
    my $cluster = $self->{cluster};
    my $timeout = $self->{_config}->{timeout};
    my @updated = ();
    my $rc = LDAP_SUCCESS;

    if (!defined($master->{suffix})) {
        return LDAP_NO_SUCH_OBJECT;
    }

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

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

    foreach my $dname (keys %{$cluster}) {
        if ($dname eq $self->{master}->{current}) {
            next;
        }
        if ($src_data && $dname eq $src_data) {
            next;
        }

        my ($ddn, @dinfo) = $self->_checkSyncData($dname, 'realtime', $entryStr, $func, $dn, @info);
        if (!$ddn) {
            next;
        }
        if ($dname eq 'Task' && $func eq 'modify') {
            unshift(@dinfo, 'REPLACE', 'lismPreviousEntry', $entryStr);
        }

        # replicate to the storage
        $rc = $self->_doUpdate($func, undef, 0, $ddn, @dinfo);
        if ($rc == LDAP_NO_SUCH_OBJECT) {
            if ($func eq 'delete') {
                next; 
            } elsif ($func eq 'modify') {
                $entryStr =~ s/userPassword: [^\n]+\n//i;
                my ($adddn, @addinfo) = $self->_checkSyncData($dname, 'differential', $entryStr, 'add', $dn, $entryStr);
                $rc = $self->_doUpdate('add', undef, 0, $adddn, @addinfo);
                if (!$rc) {
                    $rc = $self->_doUpdate($func, undef, 0, $ddn, @dinfo);
                }
            }
        } elsif ($rc == LDAP_ALREADY_EXISTS) {
            my @attrs = $self->_unique($dinfo[0] =~ /^([^:]+):/gmi);
            my @modinfo;
            foreach my $attr (@attrs) {
                my @values = $dinfo[0] =~ /^$attr: (.*)$/gmi;
                push(@modinfo, "REPLACE", $attr, @values);
            }
            $rc = $self->_doUpdate('modify', undef, 0, $ddn, @modinfo);
        }

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

            if ($conf->{sync}[0]->{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 => 'err', message => "Updating master entry($dn) failed: error code($rc)");
            return($rc, @updated);
        }
        push(@updated, $lism_master);
    }

    if ($conf->{sync}[0]->{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 $master = $self->{data}{$lism_master};
    my $cluster = $self->{cluster};
    my $timeout = $self->{_config}->{timeout};
    my $present_list;
    my @check_data = ();
    my $syncStatus = '';
    my %nosync_data;
    my %nosync_entries;
    my %deletedn;
    my %syncflag_cache;
    my %opFlag;
    $nosync_entries{'sync'} = {};
    $nosync_entries{'master'} = {};

    # don't return entry when the scope isn't base
    if ($scope != 0 || !defined($master->{suffix})) {
        return (0, ());
    }

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

    # get check filter
    my ($checkfilter) = ($filterStr =~ /\($syncFilterAttr=([^)]*)\)/i);
    if ($checkfilter) {
        $checkfilter =~ s/\\28/(/g;
        $checkfilter =~ s/\\29/)/g;
        $checkfilter =~ s/\\5C/\\/gi;
        if ($checkfilter !~ /^\(.+\)$/) {
            $checkfilter = "($checkfilter)";
        }
    } else {
        $checkfilter = "(objectClass=*)";
    }

    # get check base dn
    my ($checkbase) = ($filterStr =~ /\($syncBaseAttr=([^)]*)\)/i);
    if ($checkbase && $checkbase !~ /$master->{suffix}$/i) {
        return LDAP_UNWILLING_TO_PERFORM;
    }

    # check size limit
    my ($sync_size) = ($filterStr =~ /\($syncSizeAttr=([^)]*)\)/i);
    if (!$sync_size || $sync_size !~ /^[0-9]+$/) {
        $sync_size = $sizeLimit;
    }

    # check operation
    my ($sync_ops) = ($filterStr =~ /\($optionAttr=([^)]*)\)/i);
    foreach my $op ('add', 'modify', 'delete') {
        if (",$sync_ops," =~ /,$op,/i) {
            $opFlag{$op} = 1;
        }
    }

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

    # get present entry list
    $present_list = $self->_getPresentList($checkfilter, $checkbase);
    if (!defined($present_list)) {
        return LDAP_OTHER;
    }

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

            # check cluster status
            if ($cluster->{$dname}->{status} eq 'inactive') {
                next;
            }

            my $data = $self->{data}{$dname};
            my $sdata = $cluster->{$dname}->{conf};
            my $dcheckfilter = $checkfilter;
            my $dcheckbase = $checkbase;

            $dcheckfilter =~ s/$master->{suffix}/$data->{suffix}/i;
            $dcheckbase =~ s/$master->{suffix}$/$data->{suffix}/i;

            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;
                }

                if (defined($sobject->{synctype}) && $sobject->{synctype}[0] !~ /^(differential|task)$/) {
                    next;
                }

                foreach my $op (@{$sdata->{syncop}}) {
                    if (!%opFlag || defined($opFlag{$op})) {
                        $ops{$op} = 1;
                    }
                }

                foreach my $syncdn (@{$sobject->{syncdn}}) {
                    my $dbase;
                    my $sbase;

                    if ($syncdn eq '*') {
                        $dbase = $data->{suffix};
                        $sbase = $master->{suffix};
                    } else {
                        $dbase = $syncdn.','.$data->{suffix};
                        $sbase = $syncdn.','.$master->{suffix};
                    }

                    if (!defined($present_list->{$sbase})) {
                        next;
                    }

                    if ($dcheckbase && $dcheckbase !~ /$dbase$/i) {
                        next;
                    }

                    # synchronization filter
                    my $syncfilter = undef;
                    my $ocheckfilter = $dcheckfilter;
                    if (defined($sobject->{syncfilterobj})) {
                        $syncfilter = $sobject->{syncfilterobj};
                        $ocheckfilter = "(&$ocheckfilter$sobject->{syncfilter}[0])";
                    }

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

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dcheckbase ? $dcheckbase : $dbase, 2, 0, $sizeLimit, $timeout, $ocheckfilter, 0, @sync_attrs);
                    if ($rc && $rc != LDAP_NO_SUCH_OBJECT) {
                        $self->log(level => 'err', message => "Can't get values of $dname($rc)");
                        return ($rc, ());
                    }
                    my $entrynum = @entries;

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my $syncflag = 1;
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        $dn =~ tr/A-Z/a-z/;
                        if ($dcheckbase && $sobject->{dnignore}[0] ne 'on' && $dn !~ /$dcheckbase$/i) {
                            next;
                        }

                        my ($subdn) = ($dn =~ /^(.*),$dbase$/i);
                        if (!$subdn) {
                            next;
                        }

                        my ($key) = ($dn =~ /^([^,]*),/);

                        # check need for synchronization
                        if (defined($sobject->{syncflag}) && !$self->_checkSyncFlag($sobject->{syncflag}[0], "$subdn,$master->{suffix}", \%syncflag_cache)) {
                            $syncflag = 0;
                        }
 
                        my $mentry;
                        my $srcdn;
                        if ($syncflag && defined($present_list->{$sbase}{list}{$key})) {
                            if (defined($present_list->{$sbase}{list}{$key}->{$subdn})) {
                                if (!defined($syncfilter) ||
                                    LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr})) {
                                    $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                }
                            } else {
                                my @subdns = keys %{$present_list->{$sbase}{list}{$key}};
                                for (my $j = 0; $j < @subdns; $j++) {
                                    if ((defined($sobject->{unique}) && $subdns[$j] =~ /,$sobject->{unique}[0]->{base}$/i) ||
                                        $sobject->{dnignore}[0] eq 'on') {
                                        if (!defined($syncfilter) ||
                                            LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdns[$j]}->{entryStr})) {
                                            if (defined($sobject->{unique})) {
                                                $srcdn = $subdn;
                                            }
                                            $subdn = $subdns[$j];
                                            $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                        }
                                    }
                                }
                            }
                        }

                        if (!$mentry) {
                            # data storage's entry doesn't exist in master storage
                            if ($ops{delete}) {
                                $nosync_data{$dname} = 1;
                                $nosync_entries{'sync'}{$dn} = "The entry may be invalid in cluster";
                            }
                        } elsif ($srcdn) {
                            $nosync_data{$dname} = 1;
                            $nosync_entries{'sync'}{$dn} = "The entry shoud move to \"$subdn,$dbase\" in cluster";
                        } else {
                            if (!defined($sobject->{syncattrs})) {
                                @sync_attrs = $self->_unique(($mentry->{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->{type}) && $sattr->{type}[0] ne 'differential') {
                                    next;
                                }

                                if (defined($sattr->{op}) && !grep(/^modify$/, @{$sattr->{op}})) {
                                    next;
                                }

                                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $mentry->{entryStr})) {
                                    next;
                                }

                                @values = $self->_getAttrValues($mentry->{entryStr}, $attr);
                                if (defined($sattr->{option}) && grep(/^notnull$/, @{$sattr->{option}}) && !@values) {
                                    next
                                }

                                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                my $pvals = join(";", sort {lc $a cmp lc $b} @sync_vals);

                                @values = $self->_getAttrValues($entries[$i], $attr);
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($data, $master, $sattr, @values);
                                my $dvals = join(";", sort {lc $a cmp lc $b} @{$synced_vals});

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

                                my $evals = $pvals;
                                $pvals =~ s/([.*+?\[\]()|\^\$\\])/\\$1/g;
                                if ($dvals !~ /^$pvals$/i && $ops{modify}) {
                                    if ($nosync_entries{'sync'}{$dn}) {
                                        $nosync_entries{'sync'}{$dn} .= ", attr=\'$attr\' expect=\'$evals\' current=\'$dvals\'";
                                    } else {
                                        $nosync_data{$dname} = 1;
                                        $nosync_entries{'sync'}{$dn} = "The value is inconsistent in cluster: attr=\'$attr\' expect=\'$evals\' current=\'$dvals\'";
                                    }
                                }
                            }
                        }

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

                    # check added entry in present list
                    if ($ops{add} && $entrynum < $sync_size) {
                        foreach my $key (keys %{$present_list->{$sbase}{list}}) {
                            foreach my $subdn (keys %{$present_list->{$sbase}{list}{$key}}) {
                                if (defined($sobject->{syncflag}) && !$self->_checkSyncFlag($sobject->{syncflag}[0], "$subdn,$master->{suffix}", \%syncflag_cache)) {
                                    next;
                                }

                                if (defined($present_list->{$sbase}{list}{$key}->{$subdn}->{sync_present}{$dname}) ||
                                    (defined($syncfilter) &&
                                        !LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr}))) {
                                    next;
                                }

                                my $nosync_entry;

                                $nosync_data{$dname} = 1;
                                if ($sobject->{dnignore}[0] eq 'on') {
                                    $nosync_entry = "$key,$dbase";
                                } else {
                                    $nosync_entry = "$subdn,$dbase";
                                }
                                $nosync_entry =~ tr/A-Z/a-z/;
                                $nosync_entries{'sync'}{$nosync_entry} = "The entry doesn't exist in cluster";
                                $entrynum++;
                            }
                        }
                    }
                }
            }
        }
    }

    if ($base !~ /^$cluster_syncrdn/) {
        # check master data
        foreach my $dname (@check_data) {
            if ($dname eq $self->{master}->{current}) {
                next;
            }

            # check cluster status
            if ($cluster->{$dname}->{status} eq 'inactive') {
                next;
            }

            my $data = $self->{data}{$dname};
            my $sdata = $cluster->{$dname}->{conf};
            my $dcheckfilter = $checkfilter;
            my $dcheckbase = $checkbase;

            $dcheckfilter =~ s/$master->{suffix}/$data->{suffix}/i;
            $dcheckbase =~ s/$master->{suffix}$/$data->{suffix}/i;

            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;
                    my $sbase;

                    if ($masterdn eq '*') {
                        $dbase = $data->{suffix};
                        $sbase = $master->{suffix};
                    } else {
                        $dbase = $masterdn.','.$data->{suffix};
                        $sbase = $masterdn.','.$master->{suffix};
                    }

                    if ($dcheckbase && $dcheckbase !~ /$dbase$/i) {
                        next;
                    }

                    # synchronization filter
                    my $masterfilter = undef;
                    my $ocheckfilter = $dcheckfilter;
                    if (defined($sobject->{masterfilterobj})) {
                        $masterfilter = $sobject->{masterfilterobj};
                        $ocheckfilter = "(&$ocheckfilter$sobject->{masterfilter}[0])";
                    }

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

                    my $entrynum = $present_list->{$sbase}{'count'};

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

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dcheckbase ? $dcheckbase : $dbase, 2, 0, $sizeLimit, $timeout, $ocheckfilter, 0, @sync_attrs);
                    if ($rc && $rc != LDAP_NO_SUCH_OBJECT) {
                        $self->log(level => 'err', message => "Can't get values of $dname($rc)");
                        return ($rc, ());
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        $dn =~ tr/A-Z/a-z/;
                        if ($dcheckbase && $sobject->{dnignore}[0] ne 'on' && $dn !~ /$dcheckbase$/i) {
                            next;
                        }

                        my ($subdn) = ($dn =~ /^(.*),$dbase$/i);
                        if (!$subdn) {
                            next;
                        }

                        my ($key) = ($dn =~ /^([^,]*),/);

                        my $mentry;
                        my $dstdn;
                        if (defined($present_list->{$sbase}{list}{$key})) {
                            if (defined($present_list->{$sbase}{list}{$key}->{$subdn})) {
                                if (!defined($masterfilter) ||
                                    LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr})) {
                                    $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                }
                            } else {
                                my @subdns = keys %{$present_list->{$sbase}{list}{$key}};
                                for (my $j = 0; $j < @subdns; $j++) {
                                    if ((defined($sobject->{unique}) && $subdns[$j] =~ /,$sobject->{unique}[0]->{base}$/i) ||
                                        $sobject->{dnignore}[0] eq 'on') {
                                        if (!defined($masterfilter) ||
                                            LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{list}{$key}->{$subdns[$j]}->{entryStr})) {
                                            if (defined($sobject->{unique})) {
                                                $dstdn = $subdn;
                                            }
                                            $subdn = $subdns[$j];
                                            $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                        }
                                    }
                                }
                            }
                        }

                        if (!$mentry) {
                            # data storage's entry doesn't exist in master storage
                            if ($ops{add} && $entrynum < $sync_size) {
                                $nosync_data{$dname} = 1;
                                $nosync_entries{'master'}{$dn} = "The entry doesn't exist in master";
                                $entrynum++;
                            }
                        } elsif ($dstdn) {
                            $nosync_data{$dname} = 1;
                            $nosync_entries{'master'}{$dn} = "The entry should move from \"$subdn,$sbase\" in master";
                        } else {
                            if (!defined($sobject->{masterattrs})) {
                                @sync_attrs = $self->_unique(($mentry->{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];
                                }

                                if (defined($sattr->{op}) && !grep(/^modify$/, @{$sattr->{op}})) {
                                    next;
                                }

                                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $entries[$i])) {
                                    next;
                                }

                                @values = $self->_getAttrValues($entries[$i], $attr);
                                if (defined($sattr->{option}) && grep(/^notnull$/, @{$sattr->{option}}) && !@values) {
                                    next
                                }

                                my @sync_vals = $self->_checkSyncAttrs($data, $master, $sattr, @values);
                                my $dvals = join(";", sort {lc $a cmp lc $b} @sync_vals);

                                @values = $self->_getAttrValues($mentry->{entryStr}, $attr);
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($master, $data, $sattr, @values);
                                my $pvals = join(";", sort {lc $a cmp lc $b} @{$synced_vals});

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

                                my $cvals = $pvals;
                                $pvals =~ s/([.*+?\[\]()|\^\$\\])/\\$1/g;
                                if ($dvals !~ /^$pvals$/i && $ops{modify}) {
                                    if ($nosync_entries{'master'}{$dn}) {
                                        $nosync_entries{'master'}{$dn} .= ", attr=\'$attr\' expect=\'$dvals\' current=\'$cvals\'";
                                    } else {
                                        $nosync_data{$dname} = 1;
                                        $nosync_entries{'master'}{$dn} = "The value is inconsistent in master: attr=\'$attr\' expect=\'$dvals\' current=\'$cvals\'";
                                    }
                                }
                            }
                        }

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

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

            # synchronization filter
            my $masterfilter = undef;
            if (defined($sobject->{masterfilterobj})) {
                $masterfilter = $sobject->{masterfilterobj};
            }

            foreach my $key (keys %{$present_list->{$sbase}{list}}) {
                foreach my $subdn (keys %{$present_list->{$sbase}{list}{$key}}) {
                    if (!$present_list->{$sbase}{list}{$key}->{$subdn}->{present}) {
                        if (defined($masterfilter) &&
                            !LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr})) {
                            next;
                        }
                        my $nosync_entry;
                        ($nosync_entry = "$subdn,$sbase") =~ tr/A-Z/a-z/;
                        $nosync_data{$deletedn{$sbase}[0]} = 1;
                        $nosync_entries{'master'}{$nosync_entry} = "The entry may be invalid in master";
                    }
                }
            }
        }
    }

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

    return (LDAP_SUCCESS, ($syncentry));
}

sub _setSyncInfo
{
    my $self = shift;
    my ($dn, @list) = @_;
    my $conf = $self->{_lism};
    my $master = $self->{data}{$lism_master};
    my $cluster = $self->{cluster};
    my $timeout = $self->{_config}->{timeout};
    my $present_list;
    my @sync_data = ();
    my %deletedn;
    my %syncflag_cache;
    my %opFlag;
    my $continueFlag = 0;
    my $rc = LDAP_SUCCESS;

    if (!defined($master->{suffix})) {
        return LDAP_UNWILLING_TO_PERFORM;
    }

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

    # get synchronized data
    my ($sync_dnames) = ($modinfo =~ /DELETE#$syncDataAttr#(.*)#?(ADD|DELETE|REPLACE|)/i);

    # get check filter
    my ($checkfilter) = ($modinfo =~ /REPLACE#$syncFilterAttr#([^#]*)#?(ADD|DELETE|REPLACE|)/i);

    # get check base dn
    my ($checkbase) = ($modinfo =~ /REPLACE#$syncBaseAttr#([^#]*)#?(ADD|DELETE|REPLACE|)/i);
    if ($checkbase && $checkbase !~ /$master->{suffix}$/i) {
        return LDAP_UNWILLING_TO_PERFORM;
    }

    # check size limit
    my ($sync_size) = ($modinfo =~ /#$syncSizeAttr#([^#]*)#?(ADD|DELETE|REPLACE|)/i);
    if (!$sync_size || $sync_size !~ /^[0-9]+$/) {
        $sync_size = $sizeLimit;
    }

    # check synchronization option
    foreach my $op ('add', 'modify', 'delete') {
        if ($modinfo =~ /#$optionAttr#$op(#|)/i) {
            $opFlag{$op} = 1;
        }
    }

    # check continue option
    if ($modinfo =~ /#$optionAttr#continue(#|)/i) {
        $continueFlag = 1;
    }

    if ($sync_dnames) {
        foreach my $dname (keys %{$cluster}) {
            if ("#$sync_dnames#" =~ /#$dname#/i) {
                push(@sync_data, $dname);
            }
        }
    } else {
        if (!$checkfilter && !$checkbase && $modinfo !~ /REPLACE#$syncInfoAttr#sync/i) {
            return LDAP_UNWILLING_TO_PERFORM;
        }
        @sync_data = keys %{$cluster};
    }

    if ($checkfilter) {
        $checkfilter =~ s/\\28/(/g;
        $checkfilter =~ s/\\29/)/g;
        $checkfilter =~ s/\\5C/\\/gi;
        if ($checkfilter !~ /^\(.+\)$/) {
            $checkfilter = "($checkfilter)";
        }
    } else {
        $checkfilter = "(objectClass=*)";
    }

    if ($dn !~ /^$master_syncrdn/) {
        # get present entry list
        $present_list = $self->_getPresentList($checkfilter, $checkbase);
        if (!defined($present_list)) {
            return LDAP_OTHER;
        }

        foreach my $dname (@sync_data) {
            if ($dname eq $self->{master}->{current}) {
                next;
            }

            # check cluster status
            if ($cluster->{$dname}->{status} eq 'inactive') {
                next;
            }

            my $data = $self->{data}{$dname};
            my $sdata = $cluster->{$dname}->{conf};
            my $dcheckfilter = $checkfilter;
            my $dcheckbase = $checkbase;

            $dcheckfilter =~ s/$master->{suffix}/$data->{suffix}/i;
            $dcheckbase =~ s/$master->{suffix}$/$data->{suffix}/i;

            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;
                }

                if (defined($sobject->{synctype}) && $sobject->{synctype}[0] !~ /^(differential|task)$/) {
                    next;
                }

                foreach my $op (@{$sdata->{syncop}}) {
                    if (!%opFlag || defined($opFlag{$op})) {
                        $ops{$op} = 1;
                    }
                }

                foreach my $syncdn (@{$sobject->{syncdn}}) {
                    my $dbase;
                    my $sbase;
                    my @delete_list;

                    if ($syncdn eq '*') {
                        $dbase = $data->{suffix};
                        $sbase = $master->{suffix};
                    } else {
                        $dbase = $syncdn.','.$data->{suffix};
                        $sbase = $syncdn.','.$master->{suffix};
                    }

                    if (!defined($present_list->{$sbase})) {
                        next;
                    }

                    if ($dcheckbase && $dcheckbase !~ /$dbase$/i) {
                        next;
                    }

                    # synchronization filter
                    my $syncfilter = undef;
                    my $ocheckfilter = $dcheckfilter;
                    if (defined($sobject->{syncfilterobj})) {
                        $syncfilter = $sobject->{syncfilterobj};
                        $ocheckfilter = "(&$ocheckfilter$sobject->{syncfilter}[0])";
                    }

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

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dcheckbase ? $dcheckbase : $dbase, 2, 0, $sizeLimit, $timeout, $ocheckfilter, 0, @sync_attrs);
                    if ($rc && $rc != LDAP_NO_SUCH_OBJECT) {
                        $self->log(level => 'err', message => "Can't get values of $dname($rc)");
                        return $rc;
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        my $syncflag = 1;
                        my ($dn) = ($entries[$i] =~ /^dn: (.*)\n/);
                        $dn =~ tr/A-Z/a-z/;
                        if ($dcheckbase && $sobject->{dnignore}[0] ne 'on' && $dn !~ /$dcheckbase$/i) {
                            next;
                        }

                        my ($subdn) = ($dn =~ /^(.*),$dbase$/i);
                        if (!$subdn) {
                            next;
                        }

                        my ($key) = ($dn =~ /^([^,]*),/);

                        if (defined($sobject->{syncflag}) && !$self->_checkSyncFlag($sobject->{syncflag}[0], "$subdn,$master->{suffix}", \%syncflag_cache)) {
                            $syncflag = 0;
                        }

                        my $mentry;
                        my $srcdn;
                        if ($syncflag && defined($present_list->{$sbase}{list}{$key})) {
                            if (defined($present_list->{$sbase}{list}{$key}->{$subdn})) {
                                if (!defined($syncfilter) ||
                                    LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr})) {
                                    $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                }
                            } else {
                                my @subdns = keys %{$present_list->{$sbase}{list}{$key}};
                                for (my $j = 0; $j < @subdns; $j++) {
                                    if ((defined($sobject->{unique}) && $subdns[$j] =~ /,$sobject->{unique}[0]->{base}$/i) ||
                                        $sobject->{dnignore}[0] eq 'on') {
                                        if (!defined($syncfilter) ||
                                            LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdns[$j]}->{entryStr})) {
                                            if (defined($sobject->{unique})) {
                                                $srcdn = $subdn;
                                            }
                                            $subdn = $subdns[$j];
                                            $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                        }
                                    }
                                }
                            }
                        }

                        if (!$mentry) {
                            if ($ops{delete}) {
                                my $level = split(/,/, $subdn);
                                if (!defined($delete_list[$level])) {
                                    $delete_list[$level] = [];
                                }
                                push(@{$delete_list[$level]}, $dn); 
                            }
                        } else {
                            if ($srcdn) {
                                my $newpdn = "$subdn,$dbase";
                                $newpdn =~ s/^[^,]+,//;
                                $rc = $self->_doUpdate('modify', undef, 1, $dn, ('REPLACE', 'lismparentdn', $newpdn));
                                if ($rc) {
                                    $self->_writeSyncFail('modify', $self->{master}->{current}, $dn, ('REPLACE', 'lismparentdn', $newpdn));
                                } else {
                                    $dn = "$subdn,$dbase";
                                }
                            }

                            # modify entry which isn't equal to master storage
                            my @modlist;

                            if (!defined($sobject->{syncattrs})) {
                                @sync_attrs = $self->_unique(($mentry->{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->{type}) && $sattr->{type}[0] ne 'differential') {
                                    next;
                                }

                                if (defined($sattr->{op}) && !grep(/^modify$/, @{$sattr->{op}})) {
                                    next;
                                }

                                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $mentry->{entryStr})) {
                                    next;
                                }

                                @values = $self->_getAttrValues($mentry->{entryStr}, $attr);
                                if (defined($sattr->{option}) && grep(/^notnull$/, @{$sattr->{option}}) && !@values) {
                                    next
                                }

                                my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                my $pvals = join(";", sort {lc $a cmp lc $b} @sync_vals);

                                @values = $self->_getAttrValues($entries[$i], $attr);
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($data, $master, $sattr, @values);
                                my $dvals = join(";", sort {lc $a cmp lc $b} @{$synced_vals});

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

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

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

                        if ($rc) {
                            $self->log(level => 'err', message => "Synchronizing \"$dn\" failed($rc)");
                            if (!$continueFlag && $rc != LDAP_CONSTRAINT_VIOLATION) {
                                return $rc;
                            }
                        }

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

                    # add entries which don't exist in data storages
                    if ($ops{add}) {
                        my @non_present_list;

                        foreach my $key (keys %{$present_list->{$sbase}{list}}) {
                            foreach my $subdn (keys %{$present_list->{$sbase}{list}{$key}}) {
                                if (defined($sobject->{syncflag}) && !$self->_checkSyncFlag($sobject->{syncflag}[0], "$subdn,$master->{suffix}", \%syncflag_cache)) {
                                    next;
                                }

                                if (defined($present_list->{$sbase}{list}{$key}->{$subdn}->{sync_present}{$dname}) ||
                                    (defined($syncfilter) &&
                                        !LISM::Storage->parseFilter($syncfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr}))) {
                                    next;
                                }

                                my $level = split(/,/, $subdn);
                                ${$non_present_list[$level]}{$subdn} = $key;
                            }
                        }

                        for (my $i = 0; $i < @non_present_list; $i++) {
                            if (!defined($non_present_list[$i])) {
                                next;
                            }

                            foreach my $subdn (keys %{$non_present_list[$i]}) {
                                my $key = ${$non_present_list[$i]}{$subdn};
                                my $nosync_entry;
                                my $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                my ($rdn_attr) = ($key =~ /^([^=]+)=/);
                                my $entryStr;

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

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

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

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

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

                                    if (defined($sattr->{type}) && $sattr->{type}[0] ne 'differential') {
                                        next;
                                    }

                                    if (defined($sattr->{op}) && !grep(/^add$/, @{$sattr->{op}})) {
                                        next;
                                    }

                                    if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $mentry->{entryStr})) {
                                        next;
                                    }

                                    my @values = $self->_getAttrValues($mentry->{entryStr}, $attr);
                                    my @sync_vals = $self->_checkSyncAttrs($master, $data, $sattr, @values);
                                    if (@sync_vals) {
                                        # ignore passowrd equality if hash type is differnt
                                        if ($attr =~ /^userpassword$/i) {
                                            if (!$self->_cmpPwdHash($lism_master, $dname, join(';', @sync_vals))) {
                                                next;
                                            }
                                        }

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

                                if ($sobject->{dnignore}[0] eq 'on') {
                                    $nosync_entry = "$key,$dbase";
                                } else {
                                    $nosync_entry = "$subdn,$dbase";
                                }

                                $rc = $self->_doUpdate('add', undef, 1, $nosync_entry, $entryStr);
                                if ($rc) {
                                    $self->log(level => 'err', message => "Synchronizing \"$nosync_entry\" failed($rc)");
                                    $self->_writeSyncFail('add', $self->{master}->{current}, $nosync_entry, $entryStr);
                                    if (!$continueFlag && $rc != LDAP_CONSTRAINT_VIOLATION) {
                                        return $rc;
                                    }
                                }
                            }
                        }
                    }

                    if ($ops{delete}) {
                        for (my $i = @delete_list; $i > 0; $i--) {
                            if (!defined($delete_list[$i -1])) {
                                next;
                            }
                            foreach my $dn (@{$delete_list[$i -1]}) {
                                # delete entry which don't exist in master storage
                                $rc = $self->_doUpdate('delete', undef, 1, $dn);
                                if ($rc) {
                                    $self->_writeSyncFail('delete', $self->{master}->{current}, $dn);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if ($dn !~ /^$cluster_syncrdn/) {
        # get new present entry list
        undef $present_list;
        $present_list = $self->_getPresentList($checkfilter, $checkbase);
        if (!defined($present_list)) {
            return LDAP_OTHER;
        }

        foreach my $dname (@sync_data) {
            if ($dname eq $self->{master}->{current}) {
                next;
            }

            # check cluster status
            if ($cluster->{$dname}->{status} eq 'inactive') {
                next;
            }

            my $data = $self->{data}{$dname};
            my $sdata = $cluster->{$dname}->{conf};
            my $dcheckfilter = $checkfilter;
            my $dcheckbase = $checkbase;

            $dcheckfilter =~ s/$master->{suffix}/$data->{suffix}/i;
            $dcheckbase =~ s/$master->{suffix}$/$data->{suffix}/i;

            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;
                    my $sbase;

                    if ($masterdn eq '*') {
                        $dbase = $data->{suffix};
                        $sbase = $master->{suffix};
                    } else {
                        $dbase = $masterdn.','.$data->{suffix};
                        $sbase = $masterdn.','.$master->{suffix};
                    }

                    if (!defined($present_list->{$sbase})) {
                        next;
                    }

                    if ($dcheckbase && $dcheckbase !~ /$dbase$/i) {
                        next;
                    }

                    # synchronization filter
                    my $masterfilter = undef;
                    my $ocheckfilter = $dcheckfilter;
                    if (defined($sobject->{masterfilterobj})) {
                        $masterfilter = $sobject->{masterfilterobj};
                        $ocheckfilter = "(&$ocheckfilter$sobject->{masterfilter}[0])";
                    }

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

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

                    # get values from data storage
                    my ($rc, @entries) = $self->_do_search($dcheckbase ? $dcheckbase : $dbase, 2, 0, $sizeLimit, $timeout, $ocheckfilter, 0, @sync_attrs);
                    if ($rc && $rc != LDAP_NO_SUCH_OBJECT) {
                        $self->log(level => 'err', message => "Can't get values of $dname($rc)");
                        return $rc;
                    }

                    # comare data storage's values with master one
                    for (my $i = 0; $i < @entries; $i++) {
                        if ($dcheckbase && $sobject->{dnignore}[0] ne 'on' && $entries[$i] !~ /^dn: [^\n]*$dcheckbase\n/i) {
                            next;
                        }

                        my ($subdn) = ($entries[$i] =~ /^dn: (.*),$dbase\n/i);
                        if (!$subdn) {
                            next;
                        }
                        $subdn =~ tr/A-Z/a-z/;

                        my ($key) = ($entries[$i] =~ /^dn: ([^,]*),/);
                        $key =~ tr/A-Z/a-z/;

                        my $mentry;
                        my $dstdn;
                        if (defined($present_list->{$sbase}{list}{$key})) {
                            if (defined($present_list->{$sbase}{list}{$key}->{$subdn})) {
                                if (!defined($masterfilter) ||
                                    LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr})) {
                                    $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                }
                            } else {
                                my @subdns = keys %{$present_list->{$sbase}{list}{$key}};
                                for (my $j = 0; $j < @subdns; $j++) {
                                    if ((defined($sobject->{unique}) && $subdns[$j] =~ /,$sobject->{unique}[0]->{base}$/i) ||
                                        $sobject->{dnignore}[0] eq 'on') {
                                        if (!defined($masterfilter) ||
                                            LISM::Storage->parseFilter($masterfilter, $present_list->{$sbase}{list}{$key}->{$subdns[$j]}->{entryStr})) {
                                            if (defined($sobject->{unique})) {
                                                $dstdn = $subdn;
                                            }
                                            $subdn = $subdns[$j];
                                            $mentry = $present_list->{$sbase}{list}{$key}->{$subdn};
                                        }
                                    }
                                }
                            }
                        } elsif ($sobject->{dnignore}[0] eq 'on') {
                            $subdn =~ s/^([^,]+).*$/$1/;
                        }

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

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

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

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

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

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

                                if (defined($sattr->{op}) && !grep(/^add$/, @{$sattr->{op}})) {
                                    next;
                                }

                                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $entries[$i])) {
                                    next;
                                }

                                my @values = $self->_getAttrValues($entries[$i], $attr);
                                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}) {
                                $rc = $self->_doUpdate('add', $dname, 1, $dn, $entryStr);
                                $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr} = $entryStr;
                                if ($rc) {
                                    $self->_writeSyncFail('add', $dname, $dn, $entryStr);
                                }
                            }
                        } else {
                            if ($dstdn) {
                                my $newpdn = "$dstdn,$sbase";
                                $newpdn =~ s/^[^,]+,//;
                                $rc = $self->_doUpdate('modify', $dname, 1, $dn, ('REPLACE', 'lismparentdn', $newpdn));
                                if ($rc) {
                                    $self->_writeSyncFail('modify', $dname, $dn, ('REPLACE', 'lismparentdn', $newpdn));
                                } else {
                                    $dn = "$dstdn,$sbase";
                                }
                            }

                            # modify entry which isn't equal to data storage
                            my @modlist;
                            my $entryStr = $mentry->{entryStr};

                            if (!defined($sobject->{masterattrs})) {
                                @sync_attrs = $self->_unique(($mentry->{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];
                                }

                                if (defined($sattr->{op}) && !grep(/^modify$/, @{$sattr->{op}})) {
                                    next;
                                }

                                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $entries[$i])) {
                                    next;
                                }

                                @values = $self->_getAttrValues($entries[$i], $attr);
                                if (defined($sattr->{option}) && grep(/^notnull$/, @{$sattr->{option}}) && !@values) {
                                    next
                                }

                                my @sync_vals = $self->_checkSyncAttrs($data, $master, $sattr, @values);
                                my $dvals = join(";", sort {lc $a cmp lc $b} @sync_vals);

                                @values = $self->_getAttrValues($mentry->{entryStr}, $attr);
                                my ($synced_vals, $left_vals) = $self->_checkSyncedAttrs($master, $data, $sattr, @values);
                                my $pvals = join(";", sort {lc $a cmp lc $b} @{$synced_vals});

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

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

                                    if (@sync_vals) {
                                        push(@modlist, ('REPLACE', $attr, @sync_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}) {
                                    $rc = $self->_doUpdate('modify', $dname, 1, $dn, @modlist);
                                    $mentry->{entryStr} = $entryStr;
                                    if ($rc) {
                                        $self->_writeSyncFail('modify', $dname, $dn, @modlist);
                                    }
                                }
                            }
                        }

                        if ($rc) {
                            $self->log(level => 'err', message => "Synchronizing \"$dn\" failed($rc)");
                            if (!$continueFlag && $rc != LDAP_CONSTRAINT_VIOLATION) {
                                return $rc;
                            }
                        }

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

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

            # synchronization filter
            my $masterfilter = undef;
            if (defined($sobject->{masterfilterobj})) {
                $masterfilter = $sobject->{masterfilterobj};
            }

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

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

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

                        if ($rc) {
                            $self->log(level => 'err', message => "Synchronizing \"$nosync_entry\" failed($rc)");
                            $self->_writeSyncFail('delete', $deletedn{$sbase}[0], $nosync_entry);
                            if (!$continueFlag) {
                                return $rc;
                            }
                        }
                    }
                }
            }
        }
    }

    return $rc;
}

sub _getPresentList
{
    my $self = shift;
    my ($filterStr, $base) = @_;
    my $conf = $self->{_lism};
    my $master = $self->{data}{$lism_master};
    my $cluster = $self->{cluster};
    my $timeout = $self->{_config}->{timeout};
    my $present_list = {};

    if (!defined($master->{suffix})) {
        return undef;
    }

    foreach my $dname (keys %{$cluster}) {
        if ($cluster->{$dname}->{status} eq 'inactive') {
            next;
        }

        my $sdata = $cluster->{$dname}->{conf};

        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;

                    if ($typedn eq '*') {
                        $sbase = $master->{suffix};
                    } else {
                        $sbase = $typedn.','.$master->{suffix};
                    }

                    if ($base && $base !~ /$sbase$/i) {
                        next;
                    }

                    if (defined($present_list->{$sbase})) {
                        next;
                    }

                    my ($rc, @entries) = $self->_do_search($base ? $base : $sbase, 2, 0, $sizeLimit, $timeout, $filterStr, 0, ());
                    if ($rc) {
                        $self->log(level => 'err', message => "Getting present entries list in $dname failed($rc)");
                        if ($rc != LDAP_NO_SUCH_OBJECT) {
                            return undef;
                        }
                    }

                    $present_list->{$sbase} = {};
                    $present_list->{$sbase}{count} = @entries;
                    $present_list->{$sbase}{list} = {};
                    for (my $i = 0; $i < @entries; $i++) {
                        my ($subdn) = ($entries[$i] =~ /^dn: (.*),$sbase\n/i);
                        if (!$subdn) {
                            next;
                        }
                        $subdn =~ tr/A-Z/a-z/;

                        my ($key) = ($entries[$i] =~ /^dn: ([^,]*),/);
                        $key =~ tr/A-Z/a-z/;

                        my $entryStr;
                        ($entryStr = $entries[$i]) =~ s/^dn:.*\n//;
                        $entryStr = $self->_decBase64Entry($entryStr);
                        $present_list->{$sbase}{list}{$key}->{$subdn}->{entryStr} = $entryStr;
                        $present_list->{$sbase}{list}{$key}->{$subdn}->{present} = 0;
                    }
                }
            }
        }
    }

    return $present_list;
}

sub _checkSyncFlag
{
    my $self = shift;
    my ($syncflag, $dn, $cache) = @_;

    if ($dn !~ /$syncflag->{match}/i) {
        return 1;
    }

    my $flagdn = $syncflag->{dn};

    my @matches = ($dn =~ /$syncflag->{match}/i);
    for (my $i = 0; $i < @matches; $i++) {
        my $num = $i + 1;
        $flagdn =~ s/\%$num/$matches[$i]/g;
    }

    if (defined(${$cache}{$flagdn})) {
        return ${$cache}{$flagdn};
    }

    my ($rc, $entryStr) = $self->_do_search($flagdn, 2, 0, 0, $self->{_config}->{timeout}, $syncflag->{filter});
    if (!$rc) {
        if ($entryStr) {
            ${$cache}{$flagdn} = 1;
            return 1;
        } else {
            ${$cache}{$flagdn} = 0;
            return 0;
        }
    } else {
        return 0;
    }
}

sub _decBase64Entry
{
    my $self = shift;
    my ($entryStr) = @_;

    while ($entryStr =~ /^([^:]+):\:\s*(.+)$/im) {
        my $attr = $1;
        my $decoded = decode_base64($2);
        if (Encode::is_utf8($entryStr)) {
            $decoded = decode('utf8', $decoded);
        }
        $decoded =~ s/(\r\n|\r|\n)/\r/g;
        $entryStr =~ s/^$attr:\:\s*.+$/$attr: $decoded/m;
    }

    return $entryStr;
}

sub _getAttrValues
{
    my $self = shift;
    my ($entryStr, $attr) = @_;
    my @values;

    $entryStr = $self->_decBase64Entry($entryStr);
    @values = $entryStr =~ /^$attr: (.*)$/gmi;

    return @values;
}

sub _checkSyncData
{
    my $self = shift;
    my ($dname, $type, $entryStr, $func, $dn, @info) = @_;
    my $conf = $self->{_lism};
    my $master = $self->{data}{$lism_master};
    my $cluster = $self->{cluster};

    # check cluster status
    if ($cluster->{$dname}->{status} eq 'inactive') {
        return undef;
    }

    my $ddn = $dn;
    my $data = $self->{data}{$dname};
    my @dinfo = ();
    my $sdata = $cluster->{$dname}->{conf};
    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}) {
        return undef;
    }

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

        foreach my $syncdn (@{$sdata->{object}{$oname}->{syncdn}}) {
            if ($syncdn eq '*') {
                $sbase = $master->{suffix};
            } else {
                $sbase = $syncdn.','.$master->{suffix};
            }
            if ($dn =~ /,$sbase$/i) {
                # check need for synchronization
                if (defined($sdata->{object}{$oname}->{syncflag})) {
                    if (!$self->_checkSyncFlag($sdata->{object}{$oname}->{syncflag}[0], $dn)) {
                        next;
                    }
                }
                if (defined($sdata->{object}{$oname}->{synctype}) && $sdata->{object}{$oname}->{synctype}[0] ne $type && $func ne 'modify') {
                    next;
                }
                if (defined($sdata->{object}{$oname}->{syncfilterobj})) {
                    if (!LISM::Storage->parseFilter($sdata->{object}{$oname}->{syncfilterobj}, "$dn\n$entryStr")) {
                        next;
                    }
                }
                $sobject = $sdata->{object}{$oname};
                last;
            }
        }
        if ($sobject) {
            last;
        }
    }

    if (!$sobject) {
        return undef;
    }

    my ($rdn_attr) = ($dn =~ /^([^=]+)=/);
    # get attributes synchronized
    if ($func eq 'add') {
        foreach my $attr ($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 = $self->_unique($info[0] =~ /^([^:]+):/gmi);
        }

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

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

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

            if (defined($sattr->{type}) && $type eq 'differential' && $sattr->{type}[0] ne $type) {
                next;
            }

            if (defined($sattr->{op}) && !grep(/^add$/, @{$sattr->{op}})) {
                next;
            }

            if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $info[0])) {
                next;
            }

            my @values = $info[0] =~ /^$attr: (.*)$/gmi;
            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]) {
            return undef;
        }
    } 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 ($attr =~ /^$rdn_attr$/i) {
                if (@values) {
                    push(@dinfo, $action, $attr, @values);
                }
                next;
            }

            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;
                }

                if (defined($sobject->{synctype}) && $sobject->{synctype}[0] ne $type &&
                    (!defined($sattr->{type}) || $sattr->{type}[0] ne $type)) {
                    next;
                }

                if (defined($sattr->{type}) && $sattr->{type}[0] ne $type) {
                    next;
                }

                if (defined($sattr->{op}) && !grep(/^modify$/, @{$sattr->{op}})) {
                    next;
                }

                if (defined($sattr->{option}) && grep(/^notnull$/, @{$sattr->{option}}) && !@values) {
                    next
                }

                if (defined($sattr->{filterobj}) && !LISM::Storage->parseFilter($sattr->{filterobj}, $entryStr)) {
                    next;
                }
            } elsif (defined($sobject->{synctype}) && $sobject->{synctype}[0] ne $type) {
                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) {
            return undef;
        }
    } else {
        if ($func eq 'delete' && $sobject->{dnignore}[0] eq 'on') {
            my ($rdn) = ($dn =~ /^([^,]+),/);
            my $num = 0;
            foreach my $syncdn (@{$sobject->{syncdn}}) {
                my $sbase;
                if ($syncdn eq '*') {
                    $sbase = $master->{suffix};
                } else {
                    $sbase = $syncdn.','.$master->{suffix};
                }
                my ($rc, @entries) = $self->_do_search($sbase, 2, 0, 0, $self->{_config}->{timeout}, "($rdn)");
                if (!$rc) {
                    $num += @entries;
                }
            }
            if ($num > 1) {
                return undef;
            }
        }

        @dinfo = @info;
    }

    # replace dn to dn in the data entry
    if ($sobject->{dnignore}[0] eq 'on') {
        my $dbase;
        ($dbase = $sbase) =~ s/$master->{suffix}$/$data->{suffix}/i;
        if ($dbase eq $data->{suffix}) {
            $ddn =~ s/^([^,]+,[^,]+),.*$/$1,$dbase/;
        } else {
            $ddn =~ s/^([^,]+),.*$/$1,$dbase/;
        }
    } else {
        $ddn =~ s/$master->{suffix}$/$data->{suffix}/i;
    }
    $ddn =~ tr/A-Z/a-z/;

    return ($ddn, @dinfo);
}

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;
            }
        }

        if (!defined($sattr->{option}) || !grep(/^noreplacedn$/, @{$sattr->{option}})) {
            # 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 ($dname1, $dname2, $val1, $val2) = @_;
    my $salt;

    my $storage1 = $self->_getStorage($dname1);
    my $storage2 = $self->_getStorage($dname2);
    if (!$storage1 || !$storage2) {
        return 0;
    }

    my $htype1 = $storage1->hashType();
    my $htype2 = $storage2->hashType();

    if ($htype1 ne 'PLAINTEXT' && $val1 !~ /^{$htype1}/) {
        return 0;
    }

    if ($htype1 eq $htype2) {
        return 1;
    } elsif ($htype1 eq 'PLAINTEXT') {
        if ($htype2 eq 'CRYPT') {
            $salt = substr($val2, length('{CRYPT}'), 2);
        }
        $val1 =~ s/^\{[\}]+\}//;
        if ($val2 eq "{$htype2}".$self->_hash($htype2, $val1, $salt)) {
            return 0;
        } else {
            return 1;
        }
    }

    return 0;
}

sub _hash
{
    my $self = shift;
    my ($htype, $value, $salt) = @_;
    my $hash;

    # hash the value
    if ($htype =~ /^CRYPT$/i) {
        $hash = crypt($value, $salt);
    } elsif ($htype =~ /^MD5$/i) {
        my $ctx = Digest::MD5->new;
        $ctx->add($value);
        $hash = $ctx->b64digest.'==';
    } elsif ($htype =~ /^SHA$/i) {
        my $ctx = Digest::SHA1->new;
        $ctx->add($value);
        $hash = $ctx->b64digest.'=';
    } else {
        $hash = $value;
    }

    return $hash;
}


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 (defined($self->{data}{$dname}->{suffix}) &&
            $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;
    }
}

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";
    }

    $ldif = encode('utf8', $ldif);
    print $fd "$ldif\n";

    close($fd);

    return 0;
}

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

    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") {
                my $value = shift @info;
                if (length($value) <= $maxvallen) {
                    push(@values, $value);
                }
            }

            if ($attr =~ /^(lismPreviousEntry|modifyTimestamp)$/i) {
                next;
            }

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

            my $mod;
            if ($attr =~ /^(userPassword|plainpassword)$/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 =~ /^([^:]*): (.*)$/);

            if ($attr =~ /^(createTimestamp|modifyTimestamp)$/) {
                next;
            }

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

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

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

    $message = "type=$type dn=\"$dn\" result=$result $message";
    if ($type eq 'bind' && $result) {
        $message = "user=\"$dn\" $message";
    } else {
        my $binddn = defined($self->{bind}{edn}) ? $self->{bind}{edn} : $self->{bind}{dn};
        $message = "user=\"$binddn\" $message";
    }

    if (defined($self->{bind}{ip})) {
        $message = "ip=$self->{bind}{ip} $message";
    } else {
        $message = "ip=127.0.0.1 $message";
    }

    if (defined($self->{_config}->{auditformat})) {
        my $format = $self->{_config}->{auditformat};
        eval "\$message =~ s/${$format}[0]/${$format}[1]/i";
    }

    if (Encode::is_utf8($message)) {
        $message = encode('utf8', $message);
    }

    return $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;
