#!/usr/bin/perl -w
######################################################################
#
# $Id: webjob-cfg-update-list,v 1.12 2012/01/07 08:01:16 mavrik Exp $
#
######################################################################
#
# Copyright 2009-2012 The WebJob Project, All Rights Reserved.
#
######################################################################
#
# Purpose: Update an existing KVP list.
#
######################################################################

use strict;
use File::Basename;
use File::Path;
use FindBin qw($Bin $RealBin); use lib ("$Bin/../lib/perl5/site_perl", "$RealBin/../lib/perl5/site_perl", "/usr/local/webjob/lib/perl5/site_perl", "/opt/local/webjob/lib/perl5/site_perl");
use Getopt::Std;
use WebJob::JqdRoutines 1.012;
use WebJob::KvpRoutines 1.029;
use WebJob::Properties;

BEGIN
{
  ####################################################################
  #
  # The Properties hash is essentially private. Those parts of the
  # program that wish to access or modify the data in this hash need
  # to call GetProperties() to obtain a reference.
  #
  ####################################################################

  my (%hProperties);

  ####################################################################
  #
  # Initialize regex variables.
  #
  ####################################################################

  $hProperties{'CommonRegexes'} = PropertiesGetGlobalRegexes();

  sub GetProperties
  {
    return \%hProperties;
  }
}

######################################################################
#
# Main Routine
#
######################################################################

  ####################################################################
  #
  # Punch in and go to work.
  #
  ####################################################################

  my ($phProperties);

  $phProperties = GetProperties();

  $$phProperties{'Program'} = basename(__FILE__);

  ####################################################################
  #
  # Get Options.
  #
  ####################################################################

  my (%hOptions);

  if (!getopts('ad:f:k:m:t:', \%hOptions))
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # The auto-create flag, '-a', is optional.
  #
  ####################################################################

  $$phProperties{'AutoCreate'} = (exists($hOptions{'a'})) ? 1 : 0;

  ####################################################################
  #
  # A Delimiter, '-d', optional.
  #
  ####################################################################

  $$phProperties{'Delimiter'} = (exists($hOptions{'d'})) ? $hOptions{'d'} : ",";

  if ($$phProperties{'Delimiter'} !~ /^[ ,:;|]$/)
  {
    print STDERR "$$phProperties{'Program'}: Error='Invalid delimiter ($$phProperties{'Delimiter'}).'\n";
    exit(2);
  }

  ####################################################################
  #
  # A config file, '-f', is required.
  #
  ####################################################################

  $$phProperties{'ConfigFile'} = (exists($hOptions{'f'})) ? $hOptions{'f'} : undef;

  if (!defined($$phProperties{'ConfigFile'}))
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # A key, '-k', is required.
  #
  ####################################################################

  $$phProperties{'Key'} = (exists($hOptions{'k'})) ? $hOptions{'k'} : undef;

  if (!defined($$phProperties{'Key'}))
  {
    Usage($$phProperties{'Program'});
  }

  ####################################################################
  #
  # The mode, '-m', is optional.
  #
  ####################################################################

  $$phProperties{'Mode'} = (exists($hOptions{'m'})) ? lc($hOptions{'m'}) : "merge";

  if (!defined($$phProperties{'Mode'}))
  {
    Usage($$phProperties{'Program'});
  }

  if ($$phProperties{'Mode'} !~ /^(?:merge|replace|remove)$/)
  {
    print STDERR "$$phProperties{'Program'}: Error='Invalid mode ($$phProperties{'Mode'}). Mode must be \"merge\", \"replace\", or \"remove\".'\n";
    exit(2);
  }

  ####################################################################
  #
  # A type, '-t', is optional.
  #
  ####################################################################

  my ($sKeyRegex, $sListRegex, $sValueRegex);

  $$phProperties{'Type'} = (exists($hOptions{'t'})) ? $hOptions{'t'} : "Generic";

  if ($$phProperties{'Type'} =~ /^Generic$/i)
  {
    $sKeyRegex = qq([\\w.-]+);
    $sValueRegex = qq([^$$phProperties{'Delimiter'}]*);
  }
  elsif ($$phProperties{'Type'} =~ /^ClientAcl$/i)
  {
    $sKeyRegex = $$phProperties{'CommonRegexes'}{'ClientId'};
    $sValueRegex = $$phProperties{'CommonRegexes'}{'Cidr'};
  }
  else
  {
    print STDERR "$$phProperties{'Program'}: Error='Invalid type ($$phProperties{'Type'}).'\n";
    exit(2);
  }
  $sListRegex = "(?:|$sValueRegex(?:[$$phProperties{'Delimiter'}]$sValueRegex)*)"; # Empty lists are allowed.

  ####################################################################
  #
  # Validate the key.
  #
  ####################################################################

  if ($$phProperties{'Key'} !~ /^$sKeyRegex$/)
  {
    print STDERR "$$phProperties{'Program'}: Error='The key ($$phProperties{'Key'}) does not pass muster.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Validate each value.
  #
  ####################################################################

  my (%hFailValues, %hPassValues);

  foreach my $sValue (@ARGV)
  {
    if ($sValue =~ /^$sValueRegex$/)
    {
      $hPassValues{$sValue}++;
    }
    else
    {
      $hFailValues{$sValue}++;
    }
  }

  if (%hFailValues)
  {
    print STDERR "$$phProperties{'Program'}: Error='One or more values (" . join($$phProperties{'Delimiter'}, map('"' . $_ . '"', sort(keys(%hFailValues)))) . ") do not pass muster.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Conditionally create a lock file. This is an exclusive lock.
  #
  ####################################################################

  my (%hLockArgs);

  if ($$phProperties{'ConfigFile'} ne "-")
  {
    %hLockArgs =
    (
      'LockFile'   => $$phProperties{'ConfigFile'} . ".lock",
      'LockRemove' => 1,
    );
    if (!KvpLockFile(\%hLockArgs))
    {
      print STDERR "$$phProperties{'Program'}: Error='$hLockArgs{'Error'}'\n";
      exit(2);
    }
  }

  ####################################################################
  #
  # Create KVP map.
  #
  ####################################################################

  my (%hKvpMap) = ();

  if (-f $$phProperties{'ConfigFile'} || $$phProperties{'ConfigFile'} eq "-")
  {
    my %hLArgs =
    (
      'File'           => $$phProperties{'ConfigFile'},
      'Properties'     => \%hKvpMap,
      'Template'       => { $sKeyRegex => $sListRegex, },
      'VerifyValues'   => 1,
    );
    if (!KvpGetKvps(\%hLArgs))
    {
      print STDERR "$$phProperties{'Program'}: Error='$hLArgs{'Error'}'\n";
      exit(2);
    }
  }
  else
  {
    print STDERR "$$phProperties{'Program'}: Error='File ($$phProperties{'ConfigFile'}) does not exist or is not regular.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Make sure that the specified list already exists. If it doesn't
  # exist and the auto-create flag has not been set, abort.
  #
  ####################################################################

  if (!exists($hKvpMap{$$phProperties{'Key'}}) && !$$phProperties{'AutoCreate'})
  {
    print STDERR "$$phProperties{'Program'}: Error='Group ($$phProperties{'Key'}) does not exist.'\n";
    exit(2);
  }

  ####################################################################
  #
  # Conditionally replace, remove, or merge the new values.
  #
  ####################################################################

  if ($$phProperties{'Mode'} eq "replace")
  {
    $hKvpMap{$$phProperties{'Key'}} = join($$phProperties{'Delimiter'}, sort(keys(%hPassValues)));
  }
  elsif($$phProperties{'Mode'} eq "remove")
  {
    $hKvpMap{$$phProperties{'Key'}} = JqdRebuildGroup({ 'Delimiter' => $$phProperties{'Delimiter'}, 'Group' => $$phProperties{'Key'}, 'GroupMap' => \%hKvpMap, 'Members' => join($$phProperties{'Delimiter'}, sort(keys(%hPassValues))), 'RemoveMembers' => 1 });
  }
  else
  {
    $hKvpMap{$$phProperties{'Key'}} = JqdRebuildGroup({ 'Delimiter' => $$phProperties{'Delimiter'}, 'Group' => $$phProperties{'Key'}, 'GroupMap' => \%hKvpMap, 'Members' => join($$phProperties{'Delimiter'}, sort(keys(%hPassValues))), 'RemoveMembers' => 0 });
  }

  ####################################################################
  #
  # Write the new config file.
  #
  ####################################################################

  my %hLArgs =
  (
    'File'           => $$phProperties{'ConfigFile'},
    'Properties'     => \%hKvpMap,
    'Template'       => { $sKeyRegex => $sListRegex, },
    'VerifyValues'   => 0,
  );
  if (!KvpSetKvps(\%hLArgs))
  {
    print STDERR "$$phProperties{'Program'}: Error='$hLArgs{'Error'}'\n";
    exit(2);
  }

  ####################################################################
  #
  # Shutdown and go home.
  #
  ####################################################################

  if ($$phProperties{'ConfigFile'} ne "-")
  {
    KvpUnlockFile(\%hLockArgs);
  }

  1;


######################################################################
#
# Usage
#
######################################################################

sub Usage
{
  my ($sProgram) = @_;
  print STDERR "\n";
  print STDERR "Usage: $sProgram [-a] [-d delimiter] [-m {merge|replace|remove}] [-t type] -f {file|-} -k key [value [value ...]]\n";
  print STDERR "\n";
  exit(1);
}


=pod

=head1 NAME

webjob-cfg-update-list - Update an existing KVP list.

=head1 SYNOPSIS

B<webjob-cfg-update-list> B<[-a]> B<[-d delimiter]> B<[-m {merge|replace|remove}]> B<[-t type]> B<-f {file|-}> B<-k key> B<[value [value ...]]>

=head1 DESCRIPTION

This utility updates, creates, or replaces a KVP (Key/Value Pair)
list.  Duplicate values (if any) are automatically squashed.

=head1 OPTIONS

=over 4

=item B<-a>

Instructs the script to automatically create keys if they don't exist.
This option is disabled by default.

=item B<-d delimiter>

Specifies the list delimiter.  Valid delimiters include the following
characters: space ' ', comma ',', colon ':', semi-colon ';', and pipe
'|'.  The default delimiter is a comma.  Note that parse errors are
likely to occur if the specified delimiter appears in any of the
values.

=item B<-f {file|-}>

Specifies the config file to operate on.

=item B<-k key>

Specifies the name of the key to update.

=item B<-m {merge|replace|remove}>

Specifies the mode of operation.  Merge mode will cause values to be
merged into the specified list (if it exists).  Replace mode will
cause the specified list to be completely replaced by the new values.
Remove mode will cause existing values to be deleted from the
specified list.  The default mode is Merge.

=item B<-t type>

Specifies the type of config file being operated on.  Each supported
type has a custom template that is used to validate keys and values.
Currently, the following types are supported: ClientAcl and Generic.
The default type is Generic.

=back

=head1 AUTHOR

Klayton Monroe

=head1 SEE ALSO

webjob-cfg-create-list(1), webjob-cfg-delete-list(1)

=head1 LICENSE

All documentation and code are distributed under same terms and
conditions as B<WebJob>.

=cut
