#!/usr/bin/perl

use strict;
use warnings;
use Socket;
use Sys::Hostname;
use lib '/opt/novell/iprint/lib/perl5';
use Net::Domain qw(hostfqdn);
use File::Spec qw(tmpdir);
use File::Temp qw(mktemp tempdir);
use Net::LDAP::Util qw(ldap_explode_dn escape_dn_value);
use Data::Dumper;
use Getopt::Long qw(:config no_ignore_case bundling);
use XML::Simple;
use IO::File;
use IO::Select;
use IPC::Open3 qw(open3);
use URI;
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
use POSIX qw(EINTR EAGAIN);
use Cwd qw(getcwd);
require LWP::UserAgent;
use MIME::Base64 qw(encode_base64);
use MIME::Base64 qw(decode_base64);
use Term::ReadKey qw(ReadMode ReadLine);
use Encode;

use Net::LDAPS;
use Net::LDAP;
use Net::LDAP::LDIF;
use File::Path qw(rmtree);
use File::Basename qw(basename);
#use Switch;
# Includes the LDAP error codes & constants
use Net::LDAP::Constant qw( 
			LDAP_BUSY
			LDAP_UNAVAILABLE
			LDAP_UNWILLING_TO_PERFORM
			LDAP_LOCAL_ERROR
			LDAP_ENCODING_ERROR
			LDAP_SERVER_DOWN
			LDAP_TIMEOUT
			LDAP_ALREADY_EXISTS);

# Error constants, retaining the same codes from iprintmig, new codes have greater value :
# ----------------------------------------------------------------------------------------
use constant _ERROR_UNABLE_TO_READ_PASSWORD => 3;
use constant _ERROR_UNABLE_TO_READ_PRINTERS => 4;
use constant _ERROR_INVALID_INPUT_DATA      => 5;
use constant _ERROR_NO_PASSWORD_PROVIDED    => 9;
use constant _ERROR_NO_USERNAME_PROVIDED    => 10;
use constant _ERROR_INVALID_OPTIONS         => 12;
use constant _ERROR_GETTING_INPUT_XML_DATA  => 13;
use constant _ERROR_RUNNING_PSM             => 14;

# Net::LDAP::Constant has error codes defined till num 97, so using later numbers :
# ----------------------------------------------------------------------------------------
use constant _ERROR_XML_PARSING_FAILED      => 98;
use constant _ERROR_EDIR_CERT_NOT_EXISTS    => 99;
use constant _ERROR_EDIR_CERT_LIFE_ENDS     => 100;
use constant _ERROR_OPENSSL_FAILED          => 101;
use constant _ERROR_LDAP_CONNECT            => 102;
use constant _ERROR_MANAGER_INFO_MISSING    => 103;
use constant _ERROR_LDAP_SEARCH             => 104;
#use constant _ERROR_PRINTER_INFO_MISSING    => 105;

use constant _ERROR_PRINTER_XML_INFO_MISSING => 106;

use constant _ERROR_OBJ_EXISTS_WITH_DIFF_DN  => 107;
use constant _ERROR_PRINTER_ASSTD_DIFF_MAN   => 108;

use constant _ERROR_INSUFFICIENT_LDAPOP_INFO => 109;

# Warning constants.
use constant _WARN_PRINTER_OBJCLASS_NW       => 110;
use constant _WARN_ATTR_NOT_FOUND_IN_SEARCH  => 111;
use constant _WARN_LDAP_SERVER_DOWN          => 112;
use constant _ERROR_LDAP_SERVER_DOWN         => 113;
use constant _WARN_PRINTER_INFO_MISSING    => 114;

# Maximum number of retries to be carried out.
use constant _MAX_RETRY_COUNT  => 5;
# Associate an error code for this.
use constant _ERROR_MAX_RETRY_COUNT_EXCEEDED => 114;

# eDirectory certificate file.
# ----------------------------
use constant _EDIR_CERT        => '/etc/opt/novell/certs/SSCert.der';

# Assuming the script can take maximum time of a day.
use constant _CERT_REQ_LIFE    => int(24*60*60);

# New connection timeout, default is 120 seconds.
use constant _LDAP_CON_TIMEOUT => 120;

# Number of retries, if connection to server fails.
use constant _LDAP_CON_RETRY   => 10;


# Define boolean variables :
# --------------------------
use constant TRUE  => 1;
use constant FALSE => 0;

use constant _IPRINTMIGLDAPOPS  => 'iprintmig_ldap_ops';
use constant _VERSION           => '0.1';
use constant _COPYRIGHT         => 'Copyright (c) 2011 Novell Inc.';
use constant _CONTENT           => '_xml_text_content';


use constant _ASN                => "asn";
use constant _OPERATOR_ROLE      => "iPrintPrinterOperatorRole";
use constant _USER_ROLE          => "iPrintPrinterUserRole";
use constant _ENTRY_RIGHTS       => "[Entry Rights]";
use constant _ALL_ATTRS_RIGHTS   => "[All Attributes Rights]";
use constant _SUBJECT_ROOT       => "[Root]";

# Attribute names, used to create a Printer object into eDirectory.
# -----------------------------------------------------------------
use constant _PANAME   => 'iPrintPrinterPAName';
use constant _PAPSM    => 'iPrintPrinterManager';
use constant _PAIPPURI => 'iPrintPrinterIPPURI';
use constant _OBJCLASS => 'objectClass';
use constant _NETADDR  => 'networkAddress';
use constant _PACN     => 'cn';
use constant _PAACL    => 'ACL';

use constant _IPRINT_OBJ_CLASS_TYPE => 'iPrintPrinter';
use constant _NDPS_OBJ_CLASS_TYPE   => 'NDPSPrinter';

# Average time to wait for eDirectory sync-up.
# TODO : Have to comeup with a logic for calcuclating this number as well.
# ------------------------------------------------------------------------
use constant _SYNC_UP_WAIT_TIME => (1*60); # unit : seconds

# Network address type.
# Default value of NT_TCP will be used.
# -------------------------------------
use constant _NT_TCP => 9;

# Default LDAP secure port :
# --------------------------
use constant _LDAPS_PORT => 636;

# Define Privileges :
# -------------------
use constant _SUPERVISOR_RIGHTS   => 0x00000010;
use constant _BROWSE_RIGHTS       => 0x00000001;
use constant _COMPARE_ATTR_RIGHTS => 0x00000001;
use constant _READ_ATTR_RIGHTS    => 0x00000002;
use constant _SELF_ATTR_RIGHTS    => 0x00000008;

# Any global settings, like setting the environment variables etc to be made here :
# --------------------------------------------------------------------------------------------------
# To make 'print', print with newline.
local $\ = "\n";

# Global variables :
# --------------------------------------------------------------------------------------------------
my %opts  ;
my $ldap  ;
my $port  ;
my $certPem    ;
my $dsServer   ;
my $destUserDN ;
my $adminName  ;
my $needSyncUp ;
my $ldap_log_fh  ;
my $parseXMLObj  ;
my $xmlTreeInfo  ;
my $inputXMLFile ;

my $retryCounter  = 0 ;

my @pasToCreate  ;
my @pasToModify  ;

my @pasCreateRetry;
my @pasModifyRetry;

# Logger global reference :
# -------------------------
my $globalLogger;

# Logger class :
# --------------------------------------------------------------------------------------------------
{
	package PerlLDAPLogger;

	use constant FATAL     => 0;
	use constant ERROR     => 1;
	use constant WARNING   => 2;
	use constant Info      => 3;
	use constant ASSERTION => 4;

	our @level = (
			'Fatal',
			'Error',
			'Warning',
			'Information',
			'Assertion',
		     );

	sub new {
		my $class = $_[0];
		my $objref = {
			_id    => $_[1],
			_msg   => $_[2],
			_lev   => (defined($_[3]) ? $_[3] : ERROR)
		};
		bless $objref, $class;
		return $objref;
	}

	sub id {
		my ($objref, $id) = @_;
		$objref->{_id} = $id if defined($id);
		return $objref->{_id};
	}

	sub msg {
		my ($objref, $msg) = @_;
		$objref->{_msg} = $msg if defined($msg);
		return $objref->{_msg};
	}

	sub lev {
		my ($objref, $lev) = @_;
		$objref->{_lev} = $lev if defined($lev);
		return $objref->{_lev};
	}

	sub toString {
		my ($objref) = $_[0];
		my $str = $level[$objref->{_lev}];
		$str .= "($objref->{_id})" if $objref->{_id};
		$str .= ": $objref->{_msg}" if $objref->{_msg};
		return $str;
	}

	sub Time
	{
		my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
		$year = $year+1900;
		$mon = $mon+1;
		my $time = "$mday-$mon-$year:$hour:$min:$sec";
		return $time;
	}

	sub handle {
		my ($objref) = $_[0];
		#Always print fatal errors to STDERR
		if ( !$opts{'debug'} )
		{
			print STDERR $objref->toString();
		}
		else
		{
			my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
			printf $ldap_log_fh "%02d-%02d-%4d %02d:%02d:%02d  %s\n",$mday,$mon+1,$year+1900,$hour,$min,$sec,$objref->toString();
			$ldap_log_fh->flush();
		}
		exit($objref->{_id} ? $objref->{_id} : 1) if $objref->{_lev} == FATAL; #Fatal error
	}

	# A simple call should do the logging.
	sub Logger {
		my ($objref, $id, $message, $level) = @_;
		$objref->id($id);
		$objref->msg($message);
		$objref->lev($level);
		$objref->handle();
	}

}

# Helper Subroutines
# -------------------------------------------------------------------------------------------------- 
#leena Bug 845989
#Subroutine to replace "s=" or "S=" with "st=" in the DNs as ldap standard is "st" for state object
sub HandleStateObject
{
        my $value=$_[0];
        if($value =~ /S=/){
                $value=~ s/S=/st=/;
        }
        elsif($value =~ /s=/){
                $value=~ s/s=/st=/;
        }
        $_[0]=$value;
}

#Bug 988364
#Subroutine to check for certain special characters in the DNs of the printer names.
#Certain ldap operations fail if the special characters are not escaped.
sub CheckSpecialChars($$)
{
	my $inputDn = shift;
	my $printer = shift;
	
        my $printerName = $printer;
	$printerName =~ s/([+;<>\/])/\\$1/g; 
	$inputDn =~ s/\Q$printer\E/$printerName/g;

        return $inputDn;
}

# Basic usage :
# -------------------------------------------------------------------------------------------------- 
sub BasicUsage(;$)
{
	my $err = 0;
	if (@_)
	{
		my $warning = shift;
		print {*STDERR} "$warning\n" if $warning;
		$err = 1;
	}
	print {$err ? *STDERR : *STDOUT} <<EOF;

Usage: $0 -i <input-xml-file> -d <target DSServer> -U <target user> -t <target password> [-l <ssl-port> -g <log-file>]
Try `$0 --help' for more information.
EOF
exit($err);
}

# Detailed usage, printed with --help option.
# -------------------------------------------------------------------------------------------------- 
sub Usage(;$)
{
	my $err = 0;
	if (@_)
	{
		my $warning = shift;
		print {*STDERR} "$warning\n" if $warning;
		$err = 1;
	}
	print {$err ? *STDERR : *STDOUT} <<EOF;
Usage: $0 -i <input-xml-file> -d <target DSServer> -U <target user> -t <target password> [options]
Options:
-h | --help                Print this summary.
-V | --version             Print version information.
-i | --xml-input <xmlFile> Input XML file, padbtxt.xml.
-d | --dst <server>        Destination DSServer hostname or address to migrate to.
-U | --dst-user <user>     LDAP-style admin user DN for destination server (e.g. cn=admin,o=example).
-t | --dst-pass <pass>     Password of destination server user.
-T | --dst-pass-fd <fd>    File descriptor number to read destination admin password from
-l | --port <port>         ssl port.
-g | --debug <logFile>     To print debug messages to a debug file.
EOF

exit($err);
}

# Signal handler. TODO : Check what exactly has to be done after catching the signal.
# -------------------------------------------------------------------------------------------------- 
sub sig_handler {
	my $signame = shift;
	print {*STDERR} "Terminated\n" if $signame eq "TERM";
	print {*STDERR} "Interrupted\n" if $signame eq "INT";
	exit;
}

# Print the version of this script.
# -------------------------------------------------------------------------------------------------- 
sub Version(;$)
{
	my $store = $\;
	local $\ = "";
	my $want_string = shift;
	my $ver_string = _IPRINTMIGLDAPOPS." version "._VERSION;
	if (!$want_string)
	{
		$ver_string .= " "._COPYRIGHT;
		print "$ver_string\n";
		exit;
	}
	local $\ = $store;
}

# Return the ip-address from hostname.
# -------------------------------------------------------------------------------------------------- 
sub GetIpAddressFromName {
	my $host = shift;
	if (!$host) {
		$globalLogger->Logger(-1,
		 	"In GetIpAddressFromName() : empty argument passed.",
		 	PerlLDAPLogger::FATAL);
	}
 	return inet_ntoa(scalar gethostbyname($host));
}

# Return the host-address in standard numbers and dots format.
# -------------------------------------------------------------------------------------------------- 
sub GetHostAddress {
	my $hostname = hostname();
	if (!$hostname) {
		$globalLogger->Logger(-1,
		 	"In GetHostAddress() : unable to get the 'hostname'.",
		 	PerlLDAPLogger::FATAL);
	}
 	return inet_ntoa(scalar gethostbyname($hostname));
}

# Parse the input options.
# -------------------------------------------------------------------------------------------------- 
sub parseOpts {

	my ($dpfd, $pf, $password);

	# Defaults :
	
	Usage if ($#ARGV == -1);

	eval
	{
		GetOptions("h|help" => \$opts{'help'},
			   "V|version" => \$opts{'version'},
			   "A|isAppliance" => \$opts{'isAppliance'},
			   "i|xml-input=s" => \$opts{'xml-in'},
			   "d|dst=s" => \$opts{'dstDS'},
			   "U|dst-user=s" => \$opts{'dst-user'},
			   "t|dst-pass=s" => \$opts{'dst-pass'},
			   "T|dst-pass-fd=i" => \$dpfd,
			   "l|port=i" => \$opts{'port'},
			   "g|debug=s" => \$opts{'debug'}) or BasicUsage('');
	};
	Usage("Couldn't parse options: $@\n") if $@;
	Usage if $opts{'help'}; #Usage calls exit
	Version() if $opts{'version'};
	if ($opts{'debug'}) {
		open($ldap_log_fh, '>>',"$opts{'debug'}") || die "unable to open log file for writing: $!\n";
	}

	# If user pass dn name then for target admin is needed for iprintman authentication.
	my @dns       = split(/,/,$opts{'dst-user'});
	my @dstuser   = split(/=/,$dns[0]);
	my $adminName = $dstuser[1];
	if ($dpfd) {
		if (open(PASSWORD, '<&=', $dpfd)) {
		   $password = <PASSWORD>;
		   chomp($password);
		   close(PASSWORD);
		   $opts{'dst-pass'} = $password if ($password);
		} else {
		   $globalLogger->Logger(_ERROR_UNABLE_TO_READ_PASSWORD,
				"In parseOpts() : Unable to get dst password from fd $dpfd: $!",
				PerlLDAPLogger::FATAL);
		}
	}
	$opts{'dst-pass'} = $ENV{'IPRINTMIG_DST_PASSWORD'} if (!$opts{'dst-pass'}) and $ENV{'IPRINTMIG_DST_PASSWORD'};
	if ( !$opts{'dst-pass'} )
	{
		my $store = $\;
		local $\  = "";
		print "Enter Destination Server Password: ";
		ReadMode('noecho');
		chomp($opts{'dst-pass'} = ReadLine(0));
		ReadMode(0);
		print "\n";
		local $\  = $store;
	}
}

# Convert the dn to LDAP format.
# -------------------------------------------------------------------------------------------------- 
sub convertDNToLDAPForm ($) {
	my $dn = shift;
	return $dn;
}

# Splice the 'cn' from FDN and return the remaining part of FDN.
# -------------------------------------------------------------------------------------------------- 
sub spliceCN ($) {
	my $dn = shift;
	my @arr = split (/\,/, $dn);
	splice(@arr, 0, 1);
	return join (',', @arr);
}

# Form an ACL in LDAP format. Essentially forms a string and returns it.
# Inputs should be protected attribute name, subject name and privileges.
# -------------------------------------------------------------------------------------------------- 
sub formLDAPACL ($$$) {
	my $protectedAttrName = shift;
	my $subjectName       = shift;
	my $privileges        = shift;

	return join('#', $privileges, "entry", $subjectName, $protectedAttrName);
}

# A subroutine to convert the ip-address from base64 to dotted address format.
# Note : Subroutine modified to work directly with binary data received from an LDAP search also.
# -------------------------------------------------------------------------------------------------- 
sub networkAddressTranslate(;$)
{
	my $base64_str = shift;

	# The attribute needs to be properly decoded for the type
	# This approach should work for all possible values of networkAddress
	# http://www.directory-info.com/Java/edirectory/eDirectoryNetAddress.html
	# After doing a base64-decode of the data, the format is
	# 1234#data where '1234' is an ASCII string of numbers which specifies a
	# type, '#' is a separator and 'data' is binary data corresponding in length and value
	# to the address specified by the type number ('1234' in this example).

	my $error   = '';
	my $address = '';
	my $port    = -1;

	if (decode_base64($base64_str) =~ m/^(\d+)#(.*)/s || $base64_str =~ m/^(\d+)#(.*)/s)
	{
		my ($type, $binaddr) = ($1, $2);

		if ($type eq '1') # Type Internet Protocol (IP) network address
		{
			# Extract the all four bytes as each of the 4 unsigned bytes of an IP address
			$address = join(".", unpack('C4', $binaddr));
			$error = "Invalid data length for IP Address" unless length($binaddr) eq 4;
		}
		elsif ($type eq '9') # Type Transmission Control Protocol (TCP) address
		{
			# Extract the first two bytes as an unsigned short, network byte-order
			$port = unpack('n', $binaddr); 
			# Extract the last four bytes as each of the 4 unsigned bytes of an IP address
			$address = join(".", unpack('x2C4', $binaddr));
			$error   = "Invalid data length for TCP/IP Address" unless length($binaddr) eq 6;
		}
	}
	return $address;
}

# Parse the XML file - psmimport.xml .
# -------------------------------------------------------------------------------------------------- 
sub parseXMLFile ($) {

	my $xmlFileName = shift;
	# Populate the global variables:
	$parseXMLObj = XML::Simple->new();
	$xmlTreeInfo = eval { 
			$parseXMLObj->XMLin("$xmlFileName",
			KeyAttr => [],
			ForceArray => ['profile', 'driver', 'printer', 'redirectedprinter', 'member', 'option'],
			ContentKey => _CONTENT)
			};
	$globalLogger->Logger(_ERROR_XML_PARSING_FAILED,
				"In parseXMLFile() : Failed to parse XML file, '$xmlFileName' : $@",
				PerlLDAPLogger::FATAL) if ($@);
	
	# print Dumper($xmlTreeInfo);
}

# Look for the existence of print manager and its state.
# -------------------------------------------------------------------------------------------------- 
sub psmCheck {

	$globalLogger->Logger(_ERROR_MANAGER_INFO_MISSING,
				"In psmCheck() : Print Manager information missing in '$inputXMLFile'.",
				PerlLDAPLogger::FATAL) if (!$xmlTreeInfo->{'manager'});

	my %psmManager = %{$xmlTreeInfo->{'manager'}};

	my $dnXml = $psmManager{'dn'};
	$globalLogger->Logger(_ERROR_MANAGER_INFO_MISSING,
				"In psmCheck() : Print Manager information 'dn/hostname' missing in '$inputXMLFile'.",
				PerlLDAPLogger::FATAL) if (!$dnXml);

	my @arr   = split (/\,/, $dnXml);
	my $cnXml = $arr[0];

	# Convert the context to lower case.
	$dnXml = convertDNToLDAPForm($dnXml);

	my $status    = '';
	my $dnSrch    = '';
	my $attribute = '';
	my $netAddr   = '';
	my $ipSrch    = '';
	
	splice(@arr, 0, 1);
	
	my $base  = join (',', @arr);
	
	$cnXml=~s/\(/\\\(/g;
	$cnXml=~s/\)/\\\)/g;
	
	my $psmSrch = $ldap->search( base   => $base,
	                             filter => "(&(objectClass=iPrintManager) ($cnXml))",
	                             attrs  => ['status', 'networkAddress']);

	# This handles both failure in search and zero search results.
	$globalLogger->Logger($psmSrch->code,
	      		"In psmCheck() : Search for '$cnXml' in '$base' failed : ".$psmSrch->error,
	      		PerlLDAPLogger::FATAL) if ($psmSrch->code || !$psmSrch->count);

	my $hostIp = GetHostAddress;
	my $found = 0;
	foreach my $entry ($psmSrch->entries) {
		$dnSrch = decode_utf8(${$entry->{'asn'}}{'objectName'});
	
		if ( @{${$entry->{'asn'}}{'attributes'}} == 0 ) {
			$globalLogger->Logger("",
					"In psmCheck() : No Attributes found for print manager : '$dnSrch'",
					PerlLDAPLogger::WARNING);
			next;
		}
		foreach $attribute (@{${$entry->{'asn'}}{'attributes'}}) {
			if ( ${$attribute}{'type'} =~ /networkAddress/ ) {
				foreach $netAddr (@{${$attribute}{'vals'}}) {
					$ipSrch = networkAddressTranslate($netAddr);
				}
			} elsif ( ${$attribute}{'type'} =~ /status/ ) {
				foreach $status (@{${$attribute}{'vals'}}) {
					if((index (lc($dnSrch), lc($dnXml)) == -1) && ((lc($ipSrch) eq lc($hostIp)) || ($ipSrch eq ''))) {
						$globalLogger->Logger(_ERROR_RUNNING_PSM,
						"In psmCheck () : PSM $dnSrch is already running.".
						" If you want to migrate to $dnXml shutdown the running".
						" PSM and start $dnXml\n",
						PerlLDAPLogger::FATAL);
					} else {
						++$found;
					}
				}
			}
		}
	}

	$globalLogger->Logger(_ERROR_MANAGER_INFO_MISSING,
				"In psmCheck() : Unable to check for print manager's existence.",
				PerlLDAPLogger::FATAL) if (!$found);
}

# Watch out..Returning by reference.
# -------------------------------------------------------------------------------------------------- 
sub returnPAXMLInfo ($) {
	my $required = shift;
	my $returnValue;
	foreach my $printer (@{$xmlTreeInfo->{'printer'}}) {
		# print $printer->{'name'};
		if ((ref $printer->{'name'} ? undef : lc($printer->{'name'})) eq lc($required)) {
			$returnValue = $printer;
			return $returnValue;
		}
	}
	# If control reaches here, it means required printer is not found.
	# Same thing will be logged in the caller.
	return;
}

# Call should be like this : 
#  getAttributeFromXMLInfo <AttributeName> <PAInfoContainer>
#  <AttributeName>   : Name of the attribute.
#  <PAInfoContainer> : The return value call to subroutine 'returnPAXMLInfo(<PAName>)'
# ---------------------------------------------------------------------------------------------------
sub getAttributeFromXMLInfo ($$) {
	my $attrReq  = shift;
	my $infoData = shift;
	foreach my $key (keys %{$infoData}) {
		if (lc($key) eq lc($attrReq)) {
			return $infoData->{$key};
		}
	}
	# The control should not reach here.
        return;
}

# Call should be like this : 
#  getAttributeFromSearchResult <AttributeName> <PAInfoContainer>
#  <AttributeName>   : Name of the attribute.
#  <PAInfoContainer> : The value contained in the search result for the <PAName> entry.
#                      Ex : $printerSrch->entry / $printerSrch->entry(0) etc.
# ---------------------------------------------------------------------------------------------------
sub getAttributeFromSearchResult ($$) {
	my $attrReq  = shift;
	my $infoData = shift;

	foreach my $key (%{$infoData}) {
		if ($key ne _ASN) { next; }
		foreach my $ikey (%{$infoData->{$key}}) {
			if (lc($ikey) eq lc($attrReq)) {
				return $infoData->{$key}->{$ikey};
			} elsif (ref($infoData->{$key}->{$ikey}) eq "ARRAY") {
				foreach my $index (@{$infoData->{$key}->{$ikey}}) {
					my $type;
					my @values;
					foreach my $inkey (keys %{$index}) {
						if (lc($inkey) eq "type") {
							$type = $index->{$inkey};
						} elsif (lc($inkey) eq "vals") {
							foreach my $ele (@{$index->{$inkey}}) {
								push(@values, "$ele");
							}
						}
					}
					if (lc($type) eq lc($attrReq)) {
						if (scalar(@values) > 1) {
							return @values;
						}
						return "@values";
					}
				}
			}
		}
	}

	$globalLogger->Logger(_WARN_ATTR_NOT_FOUND_IN_SEARCH,
			"In getAttributeFromSearchResult() : Attribute '$attrReq' not found in search result.",
			PerlLDAPLogger::WARNING);
	return;
}


# Convert the ACL from XML file to LDAP format.
# i.e from 
#	trustee = 'cn=psm1,o=novell' and role = 'manager'
#			to
#	{16#entry#cn=psm1,o=novell#[Entry Rights]}
# ---------------------------------------------------------------------------------------------------
sub convertACLToLDAPFormat ($$) {
	my $trustee = shift;
	my $role    = shift;
	my $ldapAclString = "";
	$trustee = convertDNToLDAPForm($trustee);
	if (lc($role) eq "operator") {
		$ldapAclString = int(_SELF_ATTR_RIGHTS)."#"."entry"."#".$trustee."#"._OPERATOR_ROLE;
	} elsif (lc($role) eq "user") {
		$ldapAclString = int(_SELF_ATTR_RIGHTS)."#"."entry"."#".$trustee."#"._USER_ROLE;
	} elsif (lc($role) eq "manager") {
		$ldapAclString = int(_SUPERVISOR_RIGHTS)."#"."entry"."#".$trustee."#"._ENTRY_RIGHTS;
	}
	return $ldapAclString;
}


# Convert the ACL obtained from search result to XML format for comparison.
# i.e from 
#	{16#entry#cn=psm1,o=novell#[Entry Rights]}
#			to
#	trustee = 'cn=psm1,o=novell' and role = 'manager'
# ---------------------------------------------------------------------------------------------------
sub convertACLToXMLFormat ($) {
	my $srchAcl = shift;
	my @xmlForm;
	my @aclArr  = split (/\#/, $srchAcl);
	if (${aclArr}[3] eq _OPERATOR_ROLE) {
		push(@xmlForm, "operator");
	} elsif (${aclArr}[3] eq _USER_ROLE) {
		push(@xmlForm, "user");
	} elsif ((${aclArr}[3] eq _ENTRY_RIGHTS) && (${aclArr}[0] & _SUPERVISOR_RIGHTS)) {
		push(@xmlForm, "manager");
	} else {
		push(@xmlForm, "none");
	}
	push(@xmlForm, ${aclArr}[2]);
	return @xmlForm;
}


# Iteratively look for ACL in XML file for a match in ldap search result.
# Return : 0 on failure, 1 of success.
# ----------------------------------------------------------------------------------------------------
sub lookUpACLx ($$$) {
	my $searchArr = shift;
	my $trustee   = shift;
	my $role      = shift;
	$trustee      = convertDNToLDAPForm($trustee);
	my @convXMLForm;

	foreach my $acl (@{$searchArr}) {
		@convXMLForm = convertACLToXMLFormat($acl);
		if ((lc($trustee) eq lc(${convXMLForm}[1])) && ($role eq ${convXMLForm}[0])) {
			return 1;
		}
	}
	return 0;
}

# Compare each ACL in XML file for a match in search result.
# Return : A hash of ACLs not existing in search result.
# ----------------------------------------------------------------------------------------------------
sub compareACLx ($$) {
	my $xmlHash   = shift;
	my $aclSearch = shift;
        # Make a hash of arrays.
	my %printerACLDiff;
	foreach my $key (keys %{$xmlHash}) {
		my $arrOfHashs = $xmlHash->{$key};
		if (ref($arrOfHashs) eq "HASH") {
		    $arrOfHashs = [$arrOfHashs];
		}
		foreach my $index (@{$arrOfHashs}) {
			my $xmlTrustee = "";
			my $xmlRole    = "";
			foreach my $ikey (keys %{$index}) {
				if (lc($ikey) eq "trustee") {
					$xmlTrustee = $index->{$ikey};
				} elsif (lc($ikey) eq "role") {
					$xmlRole = $index->{$ikey};
				}
			}
			if (!(lookUpACLx ($aclSearch, $xmlTrustee, $xmlRole))) {
				my $addACL = convertACLToLDAPFormat($xmlTrustee, $xmlRole);
				push (@{$printerACLDiff{"@{[_PAACL]}"}}, $addACL) if ($addACL ne "");
			}
		}
	}
	return %printerACLDiff;
}

# Generating diff :
# -----------------
# Following parameters to be considered.
# 1. iPrintPrinterManager : Check with the manager dn from XML file.
# 2. iPrintPrinterIPPURI  : There can be domain-names in place of ip-address, hence
#                           a check (string match) is absolutely necessary.
# 3. networkAddress       : Compare with the printer ip-address from the 'gatewayloadstring'.
# 4. ACL                  : There might be difference in the permissions.
# Return :
# The diff hash in LDAP Modify format. (perldoc Net::LDAP, /modify)
# -------------------------------------------------------------------------------------------------- 
sub generatePrinterDiff ($$) {

	my %psmManager = %{$xmlTreeInfo->{'manager'}};
	my %printerAttrDiff;
	my %attrDiffLDAPForm;

	my $xmlPAName  = shift;
	my $srchPAName = shift;
        my $dnXML      = getAttributeFromXMLInfo("dn", $xmlPAName);
        my $dnSearch   = getAttributeFromSearchResult("objectName", $srchPAName);

        # Compare 'dn'.

	HandleStateObject($dnXML);
	if (lc($dnXML) ne lc($dnSearch)) {
		# TODO : Not sure, if this case can exist because, we do LDAP search with a base of
		#        'dn' from XML file only.
		$globalLogger->Logger(_ERROR_OBJ_EXISTS_WITH_DIFF_DN,
			"In generatePrinterDiff() : A printer object with same".
			" name already exists with different dn : $dnSearch",
			PerlLDAPLogger::ERROR);
		return %printerAttrDiff;
	}

	# Compare 'manager'.

        my $mnSearch   = getAttributeFromSearchResult("iPrintPrinterManager", $srchPAName);
        if (lc($mnSearch) ne lc($psmManager{'dn'})) {
		# TODO : Assign a proper return type for this case.
		$globalLogger->Logger(_ERROR_PRINTER_ASSTD_DIFF_MAN,
		 	"In generatePrinterDiff() : A printer object with same name already ".
			"exists and is associated with different manager : $mnSearch",
			PerlLDAPLogger::ERROR);
		return %printerAttrDiff;
	}

	# Compare 'ACL'.

	my $aclXML     = getAttributeFromXMLInfo("acl", $xmlPAName);
	my @aclSearch  = getAttributeFromSearchResult("acl", $srchPAName);

	my %temp = compareACLx($aclXML, [@aclSearch]);
	# Check for an empty hash.
	unless (!(keys %temp)) {
        	%{$printerAttrDiff{'add'}} = %temp;
	}

	# Compare 'iPrintPrinterIPPURI'.

	my $xmlIppUri  = "ipp://".$psmManager{'hostname'}."/ipp/".getAttributeFromXMLInfo("name", $xmlPAName);
	my $srchIppUri = getAttributeFromSearchResult(_PAIPPURI, $srchPAName);
	if (!$srchIppUri) {
		$globalLogger->Logger("",
			"In generatePrinterDiff() : "._PAIPPURI." attribute does not exist.",
			PerlLDAPLogger::Info);
		${$printerAttrDiff{'add'}}{"@{[_PAIPPURI]}"} = $xmlIppUri;
	} elsif ($xmlIppUri ne $srchIppUri) {
		$globalLogger->Logger("",
			"In generatePrinterDiff() : "._PAIPPURI.
			" attribute differs : XML => $xmlIppUri, Search => $srchIppUri",
			PerlLDAPLogger::Info);
	        %{$printerAttrDiff{'replace'}} = ( "@{[_PAIPPURI]}" => $xmlIppUri);
	}

	return %printerAttrDiff;
}
#juhi start
sub createContext
{
        my @printerContextArray;
        my $dn = shift;
        my $returnVal;
        my $objType;
        my $flag = 0;

        my $obj = (split(/\=/, $dn))[0];	

        if (lc($obj) eq 'dc')
        {
             $objType = 'domain';
             $flag = 1;
        }
        elsif (lc($obj) eq 'l')
        {
             $objType = 'locality';
        }
        elsif (lc($obj) eq 'c')
        {
             $objType = 'Country';
        }
        elsif (lc($obj) eq 's')
        {
             $objType = 'locality';
        }
        elsif (lc($obj) eq 'st')
        {
             $objType = 'locality';
        }
        elsif (lc($obj) eq 'ou')
        {
             $objType = 'organizationalUnit';
             $flag = 1;
        }
        elsif (lc($obj) eq 'o')
        {
             $objType = 'Organization';
             $flag = 1;
        }
        else
        {
                $globalLogger->Logger(_ERROR_INVALID_INPUT_DATA,
                                "In createContext() : Object '$objType' Type not found.",
                                PerlLDAPLogger::ERROR);
                $returnVal  = FALSE;
        }

        if ($flag)
        { 
        	# ObjectClass : Array of objType, 'ndsLoginProperties', 'ndsContainerLoginProperties', 'Top'
        	my @objCArr = ($objType, 'ndsLoginProperties', 'ndsContainerLoginProperties', 'Top');
        	push(@printerContextArray, _OBJCLASS );
        	push(@printerContextArray, [@objCArr]);
        }
        else
        {
        	# ObjectClass : Array of  objType, 'Top'                                                                  
        	my @objCArr = ($objType, 'Top');
        	push(@printerContextArray, _OBJCLASS );
        	push(@printerContextArray, [@objCArr]);
        }
        my $result = $ldap->add ( $dn,
                                  attrs => [@printerContextArray] );

        if ($result->code) {
                $globalLogger->Logger($result->code,
                                "In createContext() : LDAP Add failed on '$dn' : ".$result->error,
                                PerlLDAPLogger::ERROR);
                $returnVal  = FALSE;
        } else {
                $globalLogger->Logger("",
                                "In createContext () : '$dn' LDAP Add successful.",
                                PerlLDAPLogger::Info);
                $needSyncUp = TRUE;
                $returnVal  = TRUE;
        }
        return ($returnVal);

}
#juhi End

# Search for a printer agent. If found, check the ObjectClass type.
# If the ObjectClass is a "NDPS Printer" then, wait for 1 second and
# repeat the check till the object does not exist. (This is to allow
# the Netware PA to get renamed.)
# -------------------------------------------------------------------------------------------------- 
sub searchPAInEDir ($$) {

	my $printerSrch ;
	my $repeatSearch = TRUE;
	my $padn   = shift;
	my $paName = shift;

	my @arr = split(/\,/, $padn);
	splice(@arr, 0, 1);
	my $searchBase = join(',', @arr);

	my $ocl = _IPRINT_OBJ_CLASS_TYPE ;
	my $ocn = _NDPS_OBJ_CLASS_TYPE   ;
	my $resultCode = FALSE;

	# Repeatations to give scope for the change of ObjectClass type. 
	while ($repeatSearch) {

		$printerSrch = $ldap->search ( base   => $searchBase,
					       filter => "(&(|(objectClass=$ocl) (objectClass=$ocn)) (cn=$paName))" );

		if ($printerSrch->code) 
		{
#juhi start
			if ($opts{'isAppliance'} && ("NDS error: no such entry (-601)" eq $printerSrch->error))
			{
				$globalLogger->Logger($printerSrch->code,
					"In searchPAInEDir() : Failed to search printer '$paName' ".
					"in base '$searchBase' : ".$printerSrch->error.
					" : Will try to create '$searchBase' context for printer.",
					PerlLDAPLogger::WARNING);
				my $contextDn = $searchBase;
				my $count = 0;
				#========TODO=================
				#Instead of hardcoding exit criteria as 20, context length should 
				#be find based on some delimiter. 
				# and then context creation can go in recursive manner.
				while ($count<20)
				{
					$resultCode = createContext($contextDn);
					if ($resultCode)
					{
						last;
					}
					$globalLogger->Logger("",
                               			"In searchPAInEDir() : Failed to create container '$contextDn' for printer '$paName' creation .".
                                        	"Will try to create container object one level down before creating this.",
                               		 	 PerlLDAPLogger::Info);
					my @arr = split(/\,/, $contextDn);
					splice(@arr, 0, 1);
					$contextDn = join(',', @arr);

					$count++;
				}  

				while ($count)
				{
					$contextDn = $searchBase;
					$count--;
					my @arr = split(/\,/, $contextDn);
					splice(@arr, 0, $count);
					my $searchBase = join(',', @arr);

					$resultCode = createContext($searchBase);
					if (!$resultCode)
					{
						$globalLogger->Logger(_ERROR_INVALID_INPUT_DATA,
							"In searchPAInEDir() : Failed to search printer '$paName' ".
							"in base '$searchBase' : ".$printerSrch->error.
							" : skipping the printer.",
							PerlLDAPLogger::ERROR);
						last;
					}
				}
				if (!$resultCode)
				{
                        		last;
				}
				else
				{
					redo;
				}
			}
#juhi stop
			else
			{
				$globalLogger->Logger($printerSrch->code,
					"In searchPAInEDir() : Failed to search printer '$paName' ".
					"in base '$searchBase' : ".$printerSrch->error.
					" : skipping the printer.",
					PerlLDAPLogger::ERROR);
				last;
			}
		}
                 
		my $printerFound = 0;
		my $resultCount  = $printerSrch->count;
		if($resultCount) {
			foreach my $entry ($printerSrch->entries) {
				my $srchResDn  = getAttributeFromSearchResult("objectName", $entry);
        			my $finalPaDn = CheckSpecialChars($padn,$paName);
				if (lc("$srchResDn") eq lc("$finalPaDn")) { 
					my @existingOC = getAttributeFromSearchResult("objectClass", $entry);
					if (lc("$existingOC[0]") eq lc("$ocn")) {
						$globalLogger->Logger(_WARN_PRINTER_OBJCLASS_NW,
							"In searchPAInEDir() : ObjectClass of '$paName' is '$ocn', waiting ".
							"for printer rename operation to sync-up.",
							PerlLDAPLogger::WARNING);
						# TODO : For now, this is an infinite loop, waiting forever. Have to add a limit.
						sleep 1;
					} else {
						# Just push to the modify array, later a check for print manager will be done.
						# If it fails at that point, then skip the PA and proceed.
						$globalLogger->Logger("",
							"In searchPAInEDir() : Printer '$paName' exists, it has to be modified.",
							PerlLDAPLogger::Info);
						$repeatSearch = FALSE;
						push (@pasToModify, $paName);
						$printerFound = 1;
						last;
					}
				}
			}
		}
		if (!$printerFound) {
			$globalLogger->Logger("",
				"In searchPAInEDir() : Printer '$paName' does not exist, it has to be created.",
				PerlLDAPLogger::Info);
			$repeatSearch = FALSE;
			push (@pasToCreate, $paName);
		}
	}
}

# A subroutine to segregate the already existing and to be created Printer Agents.
# -------------------------------------------------------------------------------------------------- 
sub groupPrinterAgents {
	$globalLogger->Logger(_WARN_PRINTER_INFO_MISSING,
			"In groupPrinterAgents() : Printer information is missing in the input XML file, '$inputXMLFile'.",
			PerlLDAPLogger::WARNING) if (!$xmlTreeInfo->{'printer'});
	foreach my $printer (@{$xmlTreeInfo->{'printer'}}) {
		if (!$printer->{'dn'} || !$printer->{'name'}) {
			$globalLogger->Logger(_WARN_PRINTER_INFO_MISSING,
			"In groupPrinterAgents() : Printer information 'dn/name' is missing ".
			"in the input XML file, '$inputXMLFile', skipping the printer.",
			PerlLDAPLogger::WARNING) ;
			next;
		}
		searchPAInEDir ($printer->{'dn'}, $printer->{'name'});
	}
}

# A subroutine to act of LDAP errors which can be recovered.
# Input : LDAP Error code.
# -------------------------------------------------------------------------------------------------- 
sub takeActionOnError {
	my $ldapErrorCode = shift;
	my $returnVal     = FALSE;
#Surya Bug 1036776
#switch module deprecated replacing with if else 

       if ($ldapErrorCode == LDAP_SERVER_DOWN)
       {
       
           $globalLogger->Logger(_WARN_LDAP_SERVER_DOWN,
           "In takeActionOnError() : LDAP server is down : '$ldapErrorCode'.".
           "\nInitiating a restart ..",
           PerlLDAPLogger::WARNING);
            # NOTE : Control will not return if the call below fails.
           startLDAPServer();
           $returnVal = TRUE;
       }
       else
       {
           $globalLogger->Logger("",
           "In takeActionOnError() : No action to be taken for '$ldapErrorCode'.",
           PerlLDAPLogger::Info);
           $returnVal = TRUE;
       }



	return $returnVal;
}


# A subroutine to check the severity of LDAP error codes returned from 'add' and 'modify' operations.
# Returns : TRUE/FALSE - Severe/NotSevere
# -------------------------------------------------------------------------------------------------- 
sub checkSeverity {
	my $ldapRetCode = shift;
	my $returnVal   = TRUE ;
	if ($ldapRetCode != LDAP_BUSY        && $ldapRetCode != LDAP_UNAVAILABLE    &&
            $ldapRetCode != LDAP_TIMEOUT     && $ldapRetCode != LDAP_ENCODING_ERROR &&
	    $ldapRetCode != LDAP_LOCAL_ERROR && $ldapRetCode != LDAP_UNWILLING_TO_PERFORM) {
		$returnVal = FALSE;
	} elsif ($ldapRetCode == LDAP_SERVER_DOWN) {
		$returnVal = takeActionOnError($ldapRetCode);
	}
	return $returnVal;
}


# A subroutine to do LDAP Add operation.
# Return : TRUE/FALSE - For Success/Failure.
# -------------------------------------------------------------------------------------------------- 
sub ldapAdd {
	my $dn      = shift;
	my $printer = shift;
	my @printerAttrArray = @_;

	my $returnVal = FALSE;

	if (!$dn || !$printer || !@printerAttrArray) {
		$globalLogger->Logger(_ERROR_INSUFFICIENT_LDAPOP_INFO,
			"In ldapAdd() : Missing values for 'dn/printer name/attribute array'.",
			PerlLDAPLogger::ERROR);
		return ($returnVal, -1);
	}

	# print "Printing the attribute list to be used by ldap->add : ";
	# print Dumper(@printerAttrArray);

	HandleStateObject($dn);

        my $finalDn = CheckSpecialChars($dn,$printer);

	my $result = $ldap->add ( $finalDn,
	      		     	  attrs => [@printerAttrArray] );

	if ($result->code) {
		$globalLogger->Logger($result->code,
				"In ldapAdd() : LDAP Add failed on '$finalDn': ".$result->error,
				PerlLDAPLogger::ERROR);
		$returnVal  = FALSE;
	} else {
		$globalLogger->Logger("",
				"In ldapAdd () : '$printer' LDAP Add successful on '$finalDn'",
				PerlLDAPLogger::Info);
		$needSyncUp = TRUE;
		$returnVal  = TRUE;
	}
	return ($returnVal, $result->code);
}


# Get the FDN of the printer object to be created, use this for subsequent add commands.
# Create an iPrintPrinter object into the eDirectory with the following attributes :
#    - objectClass : iPrintPrinter
#    - cn : <pa-name> // Gets populated by default, no need to explicitly mention it.
#    - iPrintPrinterPAName : <pa-name>
#    - iPrintPrinterIPPURI : "ipp://<server-ip>/ipp/<pa-name>"
#    - iPrintPrinterManager : <FDN of Print Manager>
#    - ACL : Give Manager rights to the DS printer object "[Entry Rights]"
#    - ACL : Root rights to the iPrintPrinterIPPURI
#    - ACL : Container rights to iPrintPrinterUserRole
#
# TODO : Look for empty values for attributes in XML file viz.. ACL being empty etc.
# -------------------------------------------------------------------------------------------------- 
sub createPrinterObjects {

	my %psmManager = %{$xmlTreeInfo->{'manager'}};

	my $xmlPAInfo;
	my $flag = 1;
	foreach my $printer (@pasToCreate) {
		my @printerAttrArray;
		my ($funcRet, $ldapRet);

		$xmlPAInfo = returnPAXMLInfo($printer);
		if (!$xmlPAInfo) {
			$globalLogger->Logger(_ERROR_PRINTER_XML_INFO_MISSING,
				"In createPrinterObjects() : Information about the printer ".
				"'$printer', missing in the '$inputXMLFile' XML file, skipping the PA",
				PerlLDAPLogger::WARNING);
			next;
		}

		my $dn = getAttributeFromXMLInfo("dn"  , $xmlPAInfo);
		my $cn = getAttributeFromXMLInfo("name", $xmlPAInfo);

		if (!$dn || !$cn) {
			$globalLogger->Logger(_ERROR_PRINTER_XML_INFO_MISSING,
				"In createPrinterObjects() : Information 'dn/cn' for printer ".
				"'$printer', missing in the '$inputXMLFile' XML file, skipping the PA",
				PerlLDAPLogger::WARNING);
			next;
		}

		$dn    = convertDNToLDAPForm($dn);

		# ObjectClass : Array of 'iPrintPrinter', 'Device', and 'TOP'.
		my @objCArr = (_IPRINT_OBJ_CLASS_TYPE, 'Device', 'Top');
		push(@printerAttrArray, _OBJCLASS );
		push(@printerAttrArray, [@objCArr]);

		# pa-name attribute, same as 'cn'.
		push(@printerAttrArray, _PANAME);
		push(@printerAttrArray, [$cn]  );

		# ipp-uri.
		my $ippUri = "ipp://".$psmManager{'hostname'}."/ipp/".$cn;
		push(@printerAttrArray, _PAIPPURI);
		push(@printerAttrArray, [$ippUri]);

		# print manager.
		my $manDN = convertDNToLDAPForm($psmManager{'dn'});
		push(@printerAttrArray, _PAPSM  );
		push(@printerAttrArray, [$manDN]);

		# networkAddress
		my $hn      = $psmManager{'hostname'};
		my $ipHN    = GetIpAddressFromName("$hn");
		my $address = _NT_TCP."#".pack('nC4', 631, split(/\./, $ipHN));
		push(@printerAttrArray, _NETADDR  );
		push(@printerAttrArray, [$address]);

		# ObjectClass, iPrintPrinterName, iPrintPrinterIPPURI, iPrintPrinterManager
		# and NetworkAddress can be added simultaneously. If failed in this call then
		# proceed to next printer agent.
		# ----------------------------------------------------------------------------
		($funcRet, $ldapRet) = ldapAdd($dn, $printer, @printerAttrArray);
		if (!$funcRet) {
			if ($ldapRet == LDAP_ALREADY_EXISTS) {
				push(@pasModifyRetry, $printer)
			} elsif(checkSeverity($ldapRet)) {
				# Error is severe, no point in retry.
				$globalLogger->Logger($ldapRet,
					"In createPrinterObjects() : Failed to create printer object '$printer'.",
					PerlLDAPLogger::ERROR);
			} else {
				$globalLogger->Logger($ldapRet,
					"In createPrinterObjects() : Could not create printer object '$printer'.".
					"\nPrinter '$printer' added for retry.",
					PerlLDAPLogger::WARNING);
				# Found re-attemptable, hence pushing it to retry list.
				push(@pasCreateRetry, $printer);
			}
			next;
		} else {
			# IMP : Clear the variables.
			($funcRet, $ldapRet) = ();
			@printerAttrArray    = ();
		}

		# NOTE : Subsequently, we cannot call ldapAdd(), we need to call ldapModify().
		# ACL lists.
		my $aclHandle = getAttributeFromXMLInfo('acl', $xmlPAInfo);
		my @aclArray;
		if ($aclHandle) {
			foreach my $key (keys %{$aclHandle}) {
				my $arrOfHashs = $aclHandle->{$key};
				if (ref($arrOfHashs) eq "HASH") {
				    $arrOfHashs = [$arrOfHashs];
				}
				foreach my $index (@{$arrOfHashs}) {
					my $xmlTrustee = "";
					my $xmlRole    = "";
					foreach my $ikey (keys %{$index}) {
						if (lc($ikey) eq "trustee") {
							$xmlTrustee = $index->{$ikey};
						} elsif (lc($ikey) eq "role") {
							$xmlRole = $index->{$ikey};
						}
					}
					my $addACL = convertACLToLDAPFormat($xmlTrustee, $xmlRole);
					push (@{aclArray}, $addACL);
				}
			}
		}
		#If dn has #, it has to be escaped.
		$manDN =~ s/#/\\#/g;
		my $escDN = $dn;
		$escDN =~ s/#/\\#/g;
		if ($flag)
		{
			$destUserDN =~ s/#/\\#/g;
			$flag = 0;
		}
		# If there are no ACL entries in the XML file, then add the default ACLs
		if (!@aclArray) {
			push(@aclArray, convertACLToLDAPFormat($manDN , "manager"));
			push(@aclArray, formLDAPACL(_ENTRY_RIGHTS     , _SUBJECT_ROOT , _READ_ATTR_RIGHTS | _COMPARE_ATTR_RIGHTS));
			push(@aclArray, formLDAPACL(_ALL_ATTRS_RIGHTS , _SUBJECT_ROOT , _COMPARE_ATTR_RIGHTS));
			push(@aclArray, formLDAPACL(_USER_ROLE        , spliceCN($escDN) , _SELF_ATTR_RIGHTS));
			push(@aclArray, formLDAPACL(_ENTRY_RIGHTS     , $escDN           , _SUPERVISOR_RIGHTS));
			push(@aclArray, formLDAPACL(_ENTRY_RIGHTS     , $destUserDN   , _SUPERVISOR_RIGHTS));
			push(@aclArray, formLDAPACL(_OPERATOR_ROLE    , $destUserDN   , _SELF_ATTR_RIGHTS));
			push(@aclArray, formLDAPACL(_USER_ROLE        , $destUserDN   , _SELF_ATTR_RIGHTS));
			push(@aclArray, formLDAPACL(_PAIPPURI         , _SUBJECT_ROOT , _READ_ATTR_RIGHTS | _COMPARE_ATTR_RIGHTS));
		}


		my $needsModifyRetry = FALSE;
		# A bulk addition/modification was found to fail, if there was a failure in addition/modification
		# of one of the attributes. Hence, calling modify on every element of array. If failed in this call
		# then proceed to next printer agent, by verifying the severity.
		# -------------------------------------------------------------------------------------------------
		foreach my $element (@aclArray) {
			push(@printerAttrArray, 'add');
			push(@printerAttrArray, [("@{[_PAACL]}", [$element])]);
			($funcRet, $ldapRet) = ldapModify($escDN, $printer, @printerAttrArray);
			if (!$funcRet) {
				if(checkSeverity($ldapRet)) {
					# Error is severe, no point in retry.
					$globalLogger->Logger($ldapRet,
						"In createPrinterObjects() : Failed to add ACL attribute to printer object '$printer'.",
						PerlLDAPLogger::ERROR);
				} else {
					$globalLogger->Logger($ldapRet,
						"In createPrinterObjects() : Could not add ACL attribute to printer object '$printer'.".
						"\nPrinter '$printer' added for retry.",
						PerlLDAPLogger::WARNING);
					# Found re-attemptable, hence pushing it to retry list.
					# NOTE : Need to add the printer to 'modify' list.
					$needsModifyRetry = TRUE;
				}
				next;
			} else {
				# IMP : Clear the variables.
				($funcRet, $ldapRet) = ();
				@printerAttrArray    = ();
			}
		}
		push(@pasModifyRetry, $printer) if ($needsModifyRetry);
	}
}

# Subroutine to perform LDAP Modify operations.
# -------------------------------------------------------------------------------------------------- 
sub ldapModify {

	my $printerDN   = shift;
	my $printer     = shift;
	my @tempArrStg0 = @_;

	my $returnVal   = FALSE;

	if (!$printerDN || !$printer || !@tempArrStg0) {
		$globalLogger->Logger(_ERROR_INSUFFICIENT_LDAPOP_INFO,
			"In ldapModify() : Missing values for 'dn/printer name/attribute array'.",
			PerlLDAPLogger::ERROR);
		return ($returnVal, -1);
	}

	# print "Dumping tempArrStg0 :";
	# print Dumper(@tempArrStg0);

	HandleStateObject($printerDN);

        my $finalDn = CheckSpecialChars($printerDN, $printer);

	my $result = $ldap->modify ( $finalDn,
	      		     	     changes => [@tempArrStg0] );

	if ($result->code) {
		$globalLogger->Logger($result->code,
				"In ldapModify() : LDAP Modify failed on '$finalDn' : ".$result->error,
				PerlLDAPLogger::ERROR);
		$returnVal  = FALSE;
	} else {
		$globalLogger->Logger("",
				"In ldapModify() : '$printer' LDAP Modify successful.",
				PerlLDAPLogger::Info);
		$needSyncUp = TRUE;
		$returnVal  = TRUE;
	}
	return ($returnVal, $result->code);
}

# Get the FDN of the printer object to be created, use this for subsequent add commands.
# Look for the printer agent, and then for its print manager. If the association is with a
# different print manager, then report the same and skip the PA. Else, generate a diff
# between the XML info and LDAPSearch info, and then call modify on the PA with the diff.
# Look for following attributes :
#    - iPrintPrinterIPPURI  : XML does not have this attr, for it and then compare.
#    - iPrintPrinterManager : If associated with different PM then report and skip.
#    - ACL : Look up 'manager', 'operator' and 'user' roles.
# -------------------------------------------------------------------------------------------------- 
sub modifyPrinterObjects {

	my $xmlPAInfo ;
	my $srchPAInfo;
	my %printerAttrDiff;

	my $ocl = _IPRINT_OBJ_CLASS_TYPE;
	my $searchBase;
        my $srchEntryWithInfo;

	foreach my $printer (@pasToModify) {
		$xmlPAInfo  = returnPAXMLInfo($printer);
		if (!$xmlPAInfo) {
			$globalLogger->Logger(_ERROR_PRINTER_XML_INFO_MISSING,
				"In modifyPrinterObjects() : Information about the printer ".
				"'$printer', missing in the '$inputXMLFile' XML file, skipping the PA",
				PerlLDAPLogger::WARNING);
			next;
		}

		my $dn      = getAttributeFromXMLInfo ("dn", $xmlPAInfo);
		if (!$dn) {
			$globalLogger->Logger(_ERROR_PRINTER_XML_INFO_MISSING,
				"In modifyPrinterObjects() : Information 'dn/cn' for printer ".
				"'$printer', missing in the '$inputXMLFile' XML file, skipping the PA",
				PerlLDAPLogger::WARNING);
			next;
		}

		my @arr     = split (/\,/, $dn);
		splice(@arr, 0, 1);
		$searchBase = join (',', @arr);
		$srchPAInfo = $ldap->search ( base   => $searchBase,
				              filter => "(&(objectClass=$ocl) (cn=$printer))" );

		if ($srchPAInfo->code) {
			$globalLogger->Logger($srchPAInfo->code,
				"In modifyPrinterObjects() : Failed to search printer ".
				"'$printer' in base '$searchBase' : ".$srchPAInfo->error.
				" : skipping the printer.",
					PerlLDAPLogger::ERROR);
			push(@pasModifyRetry, $printer);
			next;
		}

		my $resultCount  = $srchPAInfo->count;
		my $printerFound = 0;

		if($resultCount) {
			foreach my $entry ($srchPAInfo->entries) {
				my $srchResDn  = getAttributeFromSearchResult("objectName", $entry);
				HandleStateObject($dn);
				my $finalDn = CheckSpecialChars($dn, $printer);
				if (lc("$srchResDn") eq lc("$finalDn")) {
					$printerFound = 1;
                                        $srchEntryWithInfo = $entry;
					last;
				}
			}
		}

		if (!$printerFound) {
			$globalLogger->Logger("",
				"In modifyPrinterObjects() : Printer '$printer' does not exist, it has to be created.",
				PerlLDAPLogger::Info);
			push (@pasCreateRetry, $printer);
			next;
		}
		
		my %temp = generatePrinterDiff ($xmlPAInfo, $srchEntryWithInfo);
		# Check for an empty hash.
		unless (keys %temp) {
			$globalLogger->Logger("", 
				"In modifyPrinterObjects() : '$printer' => No diff found. Continuing ..",
				PerlLDAPLogger::Info);
			next;
		}

		# We have non-zero differences, need to modify the existing object!
		%printerAttrDiff = %temp;

		# Call LDAP Modify with diff.
		my $printerDN = $dn;

		# Form the diff into a format, LDAP::Modify expects :
		# ---------------------------------------------------
		my @tempArrStg0;
		foreach my $k0 (keys %printerAttrDiff) {
			push(@tempArrStg0, $k0);
			my @tempArrStg1;
			if (ref($printerAttrDiff{$k0}) eq "HASH") {
				foreach my $k1 (keys %{$printerAttrDiff{$k0}}) {
					push(@tempArrStg1, $k1);
					if (ref(${$printerAttrDiff{$k0}}{$k1}) eq "ARRAY") {
						push(@tempArrStg1, [@{${$printerAttrDiff{$k0}}{$k1}}]);
					} else {
						push(@tempArrStg1, [${$printerAttrDiff{$k0}}{$k1}]);
					}
				}
			} elsif (ref($printerAttrDiff{$k0}) eq "ARRAY") {
				push(@tempArrStg1, [@{$printerAttrDiff{$k0}}]);
			} else {
				push(@tempArrStg1, [$printerAttrDiff{$k0}]);
			}
			push(@tempArrStg0, [@tempArrStg1]);
		}

		my $needsModifyRetry = FALSE;
		my ($pl1, $pl2);
		# Level 1 - add, delete, replace, changes.
		foreach my $l1 (@tempArrStg0) {
			if (ref($l1) eq "ARRAY") {
				# Level 2 - ACL, iPrintPrinterIPPURI, etc.
				foreach my $l2 (@{$l1}) {
					if (ref($l2) eq "ARRAY") {
						# Level 3 - Values of attributes.
						foreach my $l3 (@{$l2}) {
							# Further, if the reference turns
							# out to be an array, just call modify
							# with same reference.
							my ($_fr  , $_lr);
							my (@_fill, @_send);
							if (ref($l3) eq "ARRAY") {
								@_fill = ($pl2, [@{$l3}]);
							} else {
								@_fill = ($pl2, [$l3]);
							}
							push(@_send, $pl1);
							push(@_send, [@_fill]);
							($_fr, $_lr)   = ldapModify($printerDN, $printer, @_send);
							next if ($_lr == LDAP_ALREADY_EXISTS);
							$needsModifyRetry = !$_fr ? checkSeverity($_lr): $needsModifyRetry;
						}
						# Updating $pl2, here should never be encountered.
					} else {
						$pl2 = $l2;
					}
				}
			# Updating $pl1, here should never be encountered.
			} else {
				$pl1 = $l1;
			}
		}
		push(@pasModifyRetry, $printer) if ($needsModifyRetry);
	}
}

# We need to find a way to ensure sync-up of all the eDirectory replicas.
# Presently, a crude logic of sleep for _SYNC_UP_WAIT_TIME is used.
# TODO : Need to strongly address this functionality.
# -------------------------------------------------------------------------------------------------- 
sub ensureEDirectorySync () {
	if ($needSyncUp) {
		$globalLogger->Logger("",
				"In ensureEDirectorySync() : Waiting for eDirectory replica sync-up .. ",
				PerlLDAPLogger::Info);
		sleep(_SYNC_UP_WAIT_TIME);
	}
}


# A subroutine to start the LDAP Server.
# Making it a procedure, because it may need multiple calls.
# -------------------------------------------------------------------------------------------------- 
sub startLDAPServer {

	# Connect to LDAP server.
	# ------------------------------------------------------------------------------------------
	# TODO : Check with someone, if the following two executables, getSSCert and openssl are to 
	#        be checked for existence before use.
	# ------------------------------------------------------------------------------------------
	my $certFile = _EDIR_CERT;
	my $certLife = _CERT_REQ_LIFE;

	$globalLogger->Logger(_ERROR_EDIR_CERT_NOT_EXISTS,
				"In startLDAPServer() : eDirectory certificate file '$certFile' does not exist.",
				PerlLDAPLogger::FATAL) if (! -e $certFile);

	# Verify that the certificate does not expire till the job is done.
	# ------------------------------------------------------------------------------------------
	my $stdout = `openssl x509 -inform der -in $certFile -checkend $certLife`;
	$globalLogger->Logger(_ERROR_EDIR_CERT_LIFE_ENDS,
				"In startLDAPServer() : Certificate expires in the next $certLife seconds.",
				PerlLDAPLogger::FATAL) if ($stdout =~ /Certificate will expire/);

	# Delete the file created in previous call.
	unlink($certPem) if ($certPem);
	$certPem = mktemp("/tmp/SSCertXXXX");
	$certPem = "$certPem".".pem";

	`openssl x509 -inform der -in $certFile -out $certPem`;
	$globalLogger->Logger(_ERROR_OPENSSL_FAILED,
				"In startLDAPServer() : 'openssl' failed for certificate format conversion.",
				PerlLDAPLogger::FATAL) if ($? >> 8);

	my $count    = 0;
	# Connect to the server.
	# ------------------------------------------------------------------------------------------
	until ($ldap = Net::LDAPS->new($dsServer, port => $port, verify => 'require',
					cafile => $certPem, timeout => _LDAP_CON_TIMEOUT))
	{
		$globalLogger->Logger(_ERROR_LDAP_CONNECT,
				"In startLDAPServer() : Unable to connect to $dsServer:$port, $@",
				PerlLDAPLogger::FATAL) if ++$count > _LDAP_CON_RETRY;
		sleep 1;
	}
	
	$globalLogger->Logger("",
			"In startLDAPServer() : Connect to '$dsServer:$port' successful.", PerlLDAPLogger::Info);

	# Bind to LDAP server.
	# ------------------------------------------------------------------------------------------
	# For now, using hard-coded values:
	# ---------------------------------
	$destUserDN = $opts{'dst-user'};
	my $result  = $ldap->bind($destUserDN, password => $opts{'dst-pass'}, version => 3);

	$globalLogger->Logger($result->code(),
	       		"In startLDAPServer() : Error in binding to ldap server : ".$result->error,
	       		PerlLDAPLogger::FATAL) if ($result->code);

	$globalLogger->Logger("",
			"In startLDAPServer() : Bind successful.", PerlLDAPLogger::Info);
	
}

# Subroutine to do the clean up tasks viz.. closing global file handles, deleting temporary files etc.
# -------------------------------------------------------------------------------------------------- 
sub cleanUp () {
	close ($ldap_log_fh) if ($opts{'debug'});
	unlink($certPem);
}

# Global retry counter to keep track of number of retries.
# @pasCreateRetry and @pasModifyRetry will have the printers to subject to retry.
# Note : Blank the retry arrays before trying again.
# -------------------------------------------------------------------------------------------------- 
sub retryLogic {
	while ( $retryCounter < _MAX_RETRY_COUNT &&
		(@pasCreateRetry || @pasModifyRetry)) {
		if (@pasCreateRetry) {
			@pasToCreate    = @pasCreateRetry;
			@pasCreateRetry = ();
			createPrinterObjects();
		}
		if (@pasModifyRetry) {
			@pasToModify    = @pasModifyRetry;
			@pasModifyRetry = ();
			modifyPrinterObjects();
		}
		++$retryCounter;
	}
	if ($retryCounter == _MAX_RETRY_COUNT) {
		$globalLogger->Logger(_ERROR_MAX_RETRY_COUNT_EXCEEDED,
			"In retryLogic() : Printers that could not be created are  : @pasCreateRetry",
			PerlLDAPLogger::ERROR) if (@pasCreateRetry);
		$globalLogger->Logger(_ERROR_MAX_RETRY_COUNT_EXCEEDED,
			"In retryLogic() : Printers that could not be modified are : @pasModifyRetry",
			PerlLDAPLogger::ERROR) if (@pasModifyRetry);
	}
}

# Main subroutine for the script.
# -------------------------------------------------------------------------------------------------- 
sub main {

	$needSyncUp = FALSE;

	parseOpts();

	$globalLogger = PerlLDAPLogger->new();
	$globalLogger->Logger("",
			"In main () : Created a logger reference.", PerlLDAPLogger::Info);

	# Get the psmImport.xml file and parse it :
	# ------------------------------------------------------------------------------------------ 
	$inputXMLFile = $opts{'xml-in'};
	$globalLogger->Logger("",
			"In main () : Input XML file : $inputXMLFile", PerlLDAPLogger::Info);

	# Parse the xml file.
	parseXMLFile ($inputXMLFile);

	# Get ip-address and port of the server and the login credentials and connect.
	# ------------------------------------------------------------------------------------------
	$dsServer = $opts{'dstDS'};
	$port     = (!$opts{'port'})?_LDAPS_PORT:$opts{'port'};
	
	# Start the LDAP server, i.e connect() and bind():
	# ------------------------------------------------
	startLDAPServer ();

	# Check for print-manager name and status.
	# TODO : Check for return status of the function also.
	psmCheck();

	# Group the printer agents into @pasToModify and @pasToCreate.
	groupPrinterAgents();

	createPrinterObjects ();

	modifyPrinterObjects ();

	retryLogic();

	ensureEDirectorySync ();

	$ldap->unbind;

	cleanUp();
}

main ();
