#!/opt/SUNWmail/gtw/perl/bin/perl -ww

# $Id: delta_merge,v 1.18.14.1 1998/02/06 18:05:49 kevin Exp $

my $legal = <<'EOF';
This is UNPUBLISHED PROPRIETARY SOURCE CODE of Wingra Technologies
Incorporated; the contents of this file may not be disclosed to third parties,
copied or duplicated in any form, in whole or in part, without the prior
written permission of Wingra Technologies Incorporated.

Permission is hereby granted solely to the licensee for use of this source code
in its unaltered state. This source code may not be modified by licensee
except under specific direction of Wingra Technologies Incorporated. This
source code may not be given under any circumstances to non-licensees in any 
form, including source or binary. Unauthorized modification of this source 
constitutes breach of contract, which voids any potential pending support 
responsibilities by Wingra Technologies Incorporated. Divulging the exact or 
paraphrased contents of this source code to unlicensed parties either directly 
or indirectly constitutes violation of federal and international copyright and 
trade secret laws, and will be duly prosecuted to the fullest extent permitted 
under law.

This software is provided by Wingra Technologies Incorporated ``as is'' and any
express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed.  In no event shall the regents or contributors be liable for any
direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
EOF

# use 4-space tab stops to view this file. In vi, ":set ts=4".

BEGIN
{
    my $os;
    my $addPath;
    $os = `uname -s`;
    chomp $os;
    $os = lc $os;
 
    if (not defined($ENV{"__GETROOT__"}))
    {
        $! = -2;
        print STDERR "Environment variable __GETROOT__ must be set!\n";
        die "RUNTIME error encountered";
    }

    $addPath = `$ENV{"__GETROOT__"} / libDir`;
    chomp $addPath;
    unshift(@INC, $addPath);

    $addPath = `$ENV{"__GETROOT__"} / perlDir`;
    chomp $addPath;
    unshift(@INC, $addPath . "/lib/" . $os . "/5.002");
    unshift(@INC, $addPath . "/lib");
    unshift(@INC, $addPath . "/lib/site_perl/" . $os);
    unshift(@INC, $addPath . "/lib/site_perl");
}

require 5.002;		# uses perl5 features heavily
use strict;		# require variable declarations, strict refs, etc
use Getopt::Long 2.2;	# long-style command-line parser
use File::Basename;	# duplicates the basename library function

use Dirsync;		# abspath and .cf
use MsvLog;		# Missive-style logging
use MDEF;		# Missive Directory Exchange Format

=head1 NAME

delta_merge - merge the three lists of outgoing changes 

=head1 SYNOPSIS

B<delta_merge>
(B<-debug>) 
(B<-help>) 
B<-undos> I<filename> 
B<-foreign> I<filename>
B<-local> I<filename> 
B<-output> I<filename> 
B<-channel> I<channel> 
(B<-cf> I<filename>)
(B<-log> (I<filename>))

=head1 DESCRIPTION

This modules merges the three incremental lists of outgoing changes, making
sure to strip deleted attributes and to not have duplicates. Some tricky
comparisons are made to make delete-and-re-add situations work at least 
approximately correctly.

=head1 OPTIONS

=over 5

=item B<-debug>

Prints out debugging information.

=item B<-help>

Displays a brief help message.

=item B<-undos> I<filename>

I<Required>. Source file for the list of undos.

=item B<-foreign> I<filename>

I<Required>. Source file for the foreign-entry modifications.

=item B<-local> I<filename>

I<Required>. Source file for the local-entry modifications.

=item B<-output> I<filename>

I<Required>. Target file for the merged list.

=item B<-cf> I<filename>

The name of the configuration file, which includes ownership configuration 
flags.

=item B<-channel> I<channel>

I<Required>. Specifies the directory to look for files.

=item B<-log> (I<filename>)

Name of a log file for errors, warnings, and statistics. If no argument
supplied, uses the default.

=back

=cut

# appease -w and strict
undef $::opt_debug;
undef $::opt_help;

my $result;		# general-purpose result variable
my $debug;		# opt_debug
my ($lhs, $rhs);	# for regex processing
my %na;			# Native Addresses
my $line;		# index into @record
my @record;		# a record represented as a list of lines
my $newrecord;		# ref to anonymous copy of @record
my $numrecords;		# count of records processed (not much used)
my $log;		# ref to log file object
my ($foreign, $local, $undos, $output);	# MDEF references
my $operation;		# type of op to perform
my %delete;		# stored deletions for foreign
my %add;		# stored adds for local/undos
my %local;		# list of local entries found
my ($total_recs, $local_recs, $foreign_recs, $undo_recs) = (0,0,0,0);
			# counters
my ($foreign_conflicts, $local_adds, $undo_ignored) = (0,0,0);
			# more counters
my $cf;			# config file object
my $cao;		# common attribute owner
my $dsmTRANS = -3;	# Transient error - restartable
my $dsmCONFIG = -3;	# Configuration error - user intervention required

# parse the command line
$result = GetOptions("debug", "help", "undos=s", "foreign=s", "local=s",
	"output=s", "log:s", "cf=s", "channel=s");

$! = $dsmCONFIG;
die "Usage: $0 (-debug) (-help) -undos <filename> (-log (<filename>))\n" .
	"\t-foreign <filename> -local <filename> -output <filename>\n" .
	"\t-channel <channel> (-cf <filename>)\n"
	if ($::opt_help or (!$result) or (!$::opt_undos) or (!$::opt_foreign) or
	(!$::opt_local) or (!$::opt_output) or (!$::opt_channel));

# "opt_" is too annoying to type...
if (defined($::opt_debug)) {
  $debug = 1;
} else {
  # redirect STDOUT and STDERR
  open (STDOUT, ">/dev/null") || die ("Unable to redirect STDOUT!");
  open (STDERR, ">/dev/null") || die ("Unable to redirect STDERR!");
}

# define some base pathnames
my $p_bin = `$ENV{"__GETROOT__"} / binDir`;
chomp($p_bin);
$p_bin .= "/";

my $p_etc = `$ENV{"__GETROOT__"} / cfgDir`;
chomp($p_etc);
$p_etc .= "/";

my $p_ds = `$ENV{"__GETROOT__"} / dirsyncDir`;
chomp($p_ds);
$p_ds .= "/";

my $p_dsc = $p_ds . lc($::opt_channel) . "/";

my $p_log = `$ENV{"__GETROOT__"} / logDir`;
chomp($p_log);
$p_log .= "/dirsync/";


# default log directory may not exist, so create it if not there
mkdir($p_log, 0770) if ( ! -d $p_log );

############################
# end preliminaries
############################

# create a new config structure
$::opt_cf = "dirsync.cf" if not defined($::opt_cf);
$cf = Dirsync->new(&abspath($p_etc, $::opt_cf));
if (ref($cf) eq "SCALAR")
{
    $! = $dsmCONFIG;
    die "Error loading $::opt_cf: $$cf\n";
}

# confirm our channel name is actually a section
if (not grep(/$::opt_channel/, $cf->channels))
{
    $! = $dsmCONFIG;
    die "Channel \"$::opt_channel\" is not present in \n\t$::opt_cf!\n";
}


# open the log file and say hi
if (defined($::opt_log))
{
    my ($basename) = File::Basename::basename($0);

    # generate a log file name if none provided
    if ($::opt_log eq "")
    {
        $::opt_log = $p_log . MsvLog->logname(lc($::opt_channel));
    }

    $log = new MsvLog(&abspath($p_dsc, $::opt_log), "$basename\[$$\]", 1);
    $! = $dsmCONFIG;
    die $$foreign . "\n" if (ref($log) eq "SCALAR");

    $result = $log->report(MsvLog::NLS("i"), MsvLog::NLS("Starting"));
    $log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR");	
	# just check once...
}
else
{
    $log = stub MsvLog;
}

# open everything. Since open will return a scalar string on failure, check
#  for that, and abort if we get a scalar instead of an MDEF.
$foreign = MDEF->open(&abspath($p_dsc, $::opt_foreign), "r");
if (ref($foreign) eq "SCALAR")
{
	$log->abort($dsmCONFIG, $$foreign);
}
$local = MDEF->open(&abspath($p_dsc, $::opt_local), "r");
if (ref($local) eq "SCALAR")
{
	$log->abort($dsmCONFIG, $$local);
}
$undos = MDEF->open(&abspath($p_dsc, $::opt_undos), "r");
if (ref($undos) eq "SCALAR")
{
	$log->abort($dsmCONFIG, $$undos);
}
$output = MDEF->open(&abspath($p_dsc, $::opt_output), "w");
if (ref($output) eq "SCALAR")
{
	$log->abort($dsmCONFIG, $$output);
}

# read foreign records
while (@record = $foreign->read_entry())
{
	# check for error in read_entry
	if (ref($record[0]) eq "SCALAR")
	{
		$log->abort($dsmCONFIG, ${$record[0]});
	}

	$total_recs++; $foreign_recs++;

	# the first element is (always) an operation line
	$line = shift @record;

	# get the line, checking for a potential error
	if (not $line =~ /operation\s*=\s*(\w+)/i)
	{
		$log->abort($dsmCONFIG, MsvLog::NLS("No operation line in read_entry: \"%1\$s\""), 
					$line);
	}

	# store the name of the operation
	$operation = $1;

	# store the deletes in a list to be outputted later
	if ($operation eq "delete")
	{
		# note that to make an anonymous array with copied data, you need to
		#  use []s instead of ()s
		$delete{MDEF::native_key(@record)} = [@record];
		next;
	}
	elsif ($operation eq "add")
	{
		# check the adds against the deletes, and if we match, remove the
		#  delete from the list and change the current operation to change
		if (defined($delete{MDEF::native_key(@record)}))
		{
			delete $delete{MDEF::native_key(@record)};
			$operation = "change";
			$foreign_conflicts++;
		}
	}

	if ($operation eq "modify")
	{	$operation = "change"; }	# strip_modify does the rest

	# now write everything out (except the deletes)
	$result = $output->write_entry("operation=$operation", 
		&MDEF::strip_modify(@record));
	$log->abort($dsmCONFIG, $$result) if (ref($result) eq "SCALAR");
}

# now write the deletes too...
foreach $_ (values %delete)
{
	$result = $output->write_entry("operation=delete", 
		&MDEF::strip_modify(@{$_}));
	$log->abort($dsmCONFIG, $$result) if (ref($result) eq "SCALAR");
}
		
# read local
#  pass through everything but adds (store in a list)
while (@record = $local->read_entry())
{
	# check for error in read_entry
	if (ref($record[0]) eq "SCALAR")
	{
		$log->abort($dsmCONFIG, ${$record[0]});
	}

	$total_recs++; $local_recs++;

	# the first element is (always) an operation line
	$line = shift @record;

	# get the line, checking for a potential error
	if (not $line =~ /operation\s*=\s*(\w+)/i)
	{
		$log->abort($dsmCONFIG,
			    MsvLog::NLS("No operation line in read_entry: \"%1\$s\""),
			    $line);
	}

	# store the name of the operation
	$operation = $1;

	# store the names of local records to be compared vs the undos
	$local{MDEF::native_key(@record)} = 1;

	# store the adds in a list to be outputted later
	if ($operation eq "add")
	{
		$local_adds++;

		# note that to make an anonymous array with copied data, you need to
		#  use []s instead of ()s
		$add{MDEF::native_key(@record)} = [@record];

		next;
	}

	if ($operation eq "modify")
	{	$operation = "change"; }	# strip_modify does the rest

	# now write everything out (except the adds)
	$result = $output->write_entry("operation=$operation", 
		&MDEF::strip_modify(@record));
	$log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR");
}

# read undos
#  pass through everything except as follows:
#  if was in local, ignore
#  except if it was an add in local and a delete in undo, in which case if
#   the CD wins, change the add to a modify (???)
while (@record = $undos->read_entry())
{
    # check for error in read_entry
    if (ref($record[0]) eq "SCALAR")
    {
	$log->abort($dsmCONFIG, ${$record[0]});
    }

    $total_recs++; $undo_recs++;

    # the first element is (always) an operation line
    $line = shift @record;

    # get the line, checking for a potential error
    if (not $line =~ /operation\s*=\s*(\w+)/i)
    {
	$log->abort($dsmCONFIG,
                    MsvLog::NLS("No operation line in read_entry: \"%1\$s\""), $line);
    }

    # store the name of the operation
    $operation = $1;

    #  if was in local, ignore
    #  except if it was an add in local and a delete in undo, in which case if
    #   the CD wins, change the add to a change and output it
    if (defined($local{MDEF::native_key(@record)}))
    {
	$undo_ignored++;

	if (defined($add{MDEF::native_key(@record)} and $operation eq "delete"))
	{
	    delete $add{MDEF::native_key(@record)};

	    $cao = $cf->config($::opt_channel, "common_attribute_owner");
	    if ($cao eq "cd" or $cao eq "both-cd")
	    {
		$result = $output->write_entry("operation=change", 
		    &MDEF::strip_modify(@record));
		$log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR");
	    }
	    # otherwise just ignore the entry
	}
	next;
    }

    if ($operation eq "modify")
    {	$operation = "change"; }	# strip_modify does the rest

    # now write everything out (except the adds)
    $result = $output->write_entry("operation=$operation", 
	    &MDEF::strip_modify(@record));
    $log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR");
}

# finally write local adds
foreach $_ (values %add)
{
	$result = $output->write_entry("operation=add", 
		&MDEF::strip_modify(@{$_}));
	$log->abort($dsmTRANS, $$result) if (ref($result) eq "SCALAR");
}
		
$log->report(MsvLog::NLS("i"), MsvLog::NLS("Processed %1\$s records:"), $total_recs);
$log->report(MsvLog::NLS("i"), MsvLog::NLS("%1\$s foreign (%2\$s conflicted)"),
             $foreign_recs, $foreign_conflicts);
$log->report(MsvLog::NLS("i"),
	   MsvLog::NLS("%1\$s local (%2\$s additions)"),	     
	     $local_recs, $local_adds);    
$log->report(MsvLog::NLS("i"),	     
	   MsvLog::NLS("%1\$s local undos (%2\$s superceded)"),	     
             $undo_recs, $undo_ignored);
$log->report(MsvLog::NLS("i"), MsvLog::NLS("Exiting normally"));
exit(0);
