################################################################################
#
#  kPerl Core Library Group
#  Object Library for Memory<->File Synchronization
#
#  copyright (c)
#    Karol Szafranski, 2006
#    Institute of Molecular Biotechnology Jena, Dept. Genome Analysis, 2002, 2004
#  author
#    Karol Szafranski, karol.szafranski@leibniz-fli.de
#
################################################################################
#
#  DESCRIPTION
#
# - purpose:
#   abstract class for
#   synchronization between memory <-> file representations of data
#
# - usage:
#   - This is an inherit-only class that contributes architecture and methods
#     to the derived class.
#   - what needs to be done inside the inheriting object:
#     - Load() method needs to be provided
#     - Save() method needs to be provided
#     - SyncMemTouch() method needs to be called if changes are made to the
#       memory representation of the data, in order to trigger Sync() if
#
# - examples:
#   - constructor code that sets the file representation as the master
#     $this->{SyncFile} = $ArgFile;
#     $this->{SyncMaster} = 'file';
#     $this->SyncConnect($this->SyncValid());
#   - constructor code that sets the memory representation as the master
#     $this->Def($ArgData);
#     $this->{SyncMaster} = 'memory';
#     $this->{SyncFile} = $ArgFile;
#     $this->SyncConnect($this->SyncValid());
#
# - individual description of functions can be found at the beginning of the
#   code blocks
#
################################################################################
#
#  OBJECT OPERATORS
#
#   *** NONE ***
#
#
#  OBJECT METHODS  for external access
#
#   Sync          update synchronization between data and file
#                 The need for synchronization is tested on the basis of touch
#                 times.
#   SyncConnect   read/set connection status (boolean)
#   SyncDebug     output object debug
#   SyncFile      read/set file path
#   SyncMaster    read/set origin of data flow, possible: "file" or "memory"
#   SyncMemTouch  notify touch of memory instance
#   SyncMode      read/set decision mode for synchronization
#   SyncValid     return intactness of synchronization mechanism
#
#
#  OBJECT DATA STRUCTURE
#  (hash)
#
#   switch        hash reference for object switches, cf. method AddSwitch()
#     -debug        swallowed from derived object class
#     -SyncConnect  substitute for method call (set mode)
#     -SyncFile     substitute for method call (set mode)
#     -SyncMode     substitute for method call (set mode)
#     -SyncMaster   substitute for method call (set mode)
#     -SyncMemTouch substitute for method call (set mode)
#   SyncConnect   synchronization's activity status
#   SyncFile      file path
#   SyncGoingOn   flag used to avoid nested calling of Sync()
#   SyncMaster    origin of data flow: "file" / "memory"
#   SyncMode      how to decide if a synchronization has do be done
#                 once     lazy synchronization. Load only on first Sync() call,
#                          with SyncMaster=='file', or save only on destruction
#                          of object, with SyncMaster=='memory'.
#                 OnDestr  synchronize only via destructor function. This mode
#                          makes sense only with SyncMaster=='file'; it's
#                          actually a special-case alias for SyncMode "once".
#                 paranoid synchronize at every Sync() call
#                 std      default: synchronize on Sync() call if master
#                          instance has changed compared to slave instance
#   SyncTimeMemory
#                 time of last changes done to data
#   SyncTimeSync  time of data/file synchronization
#
################################################################################
#
#  FUNCTIONS, DATA
#
#   @ISA
#
# - housekeeping
#   &AddSwitch  (not exported)
#   &_LocalSwitch  see MainLib::DefaultObjHash.pm
#   &SyncFile
#   &SyncMaster
#   &SyncMode
#
# - diagnostics
#   &SyncDebug
#   &SyncValid
#
# - functionality
#   &Load  (needs to be implemented in the derived class)
#   &Save  (needs to be implemented in the derived class)
#   &SyncConnect
#   &SyncMemTouch
#   &Sync
#
#
#  STD OPTIONS
#
#  -debug      print debug protocol to STDERR
#
################################################################################
#
#  DEBUG, CHANGES, ADDITIONS
#
# - look also for notes in the header of each function block
#
################################################################################

package MainLib::DataFileSync;

# includes
use strict; #use warnings;  # OK 20040820
use MainLib::DefaultObjHash;
use MainLib::File qw(&ftime);
use Math::kCalc qw(&Min &Max);

# inheritance
our @ISA;
push @ISA, qw(MainLib::DefaultObjHash);


################################################################################
# housekeeping
################################################################################


# enter object switches
#
# INTERFACE
# - argument 1*: hash of switches
# - return val:  success status (boolean)
#
# DESCRIPTION
# - for description of object switches see on top
# - currently, object switches are not used to modify behavioural attributes.
#   The switch assignment syntax just serves as an alternative to function
#   calls.
#
sub AddSwitch {
  my ($this, %oopt) = @_;
  my ($bErr);

  # perform "silencing" of the object always at the beginning
  if (exists($oopt{-SyncConnect}) and !$oopt{-SyncConnect}) {
    $this->SyncConnect($oopt{-SyncConnect});
  }

  # loop over switches
  while (my($key,$val) = each(%oopt)) {
    if (0) {}
    elsif ($key =~ m/^-SyncFile$/) {
      $this->SyncFile($val);
    }
    elsif ($key =~ m/^-SyncMaster$/) {
      $this->SyncMaster($val);
    }
    elsif ($key =~ m/^-SyncMode$/) {
      $this->SyncMode($val);
    }
    elsif ($key =~ m/^-SyncMemTouch$/ and $val) {
      $this->SyncMemTouch();
    }
  }
  
  # perform "hottening" of the object always at the end
  if (exists($oopt{-SyncConnect}) and $oopt{-SyncConnect}) {
    $this->SyncConnect($oopt{-SyncConnect});
  }

  # return success status
  return !$bErr;
}


# read/set sync file path
#
# INTERFACE
# - argument 1*: file path (for set mode)
# - return val:  file path
#
sub SyncFile {
  my ($this, $arg) = @_;

  # set parameter
  if (defined $arg) {

    # check required read/write file permissions
    if (($this->{SyncMaster}||'') eq 'file' and ! -r($arg)) {
      printf STDERR "%s. WARNING: unable to read source file %s\n", (caller(0))[3], $arg||"''";
    }
    if (($this->{SyncMaster}||'') eq 'memory' and ! -w($arg)) {
      printf STDERR "%s. WARNING: unable to write target file %s\n", (caller(0))[3], $arg||"''";
    }

    $this->{SyncFile} = $arg;
  }

  return $this->{SyncFile};
}


# read/set defintion priority for synchronization: "file" or "memory"
#
# INTERFACE
# - argument 1*: priority side (for set mode)
# - return val:  priority side
#
sub SyncMaster {
  my ($this, $arg) = @_;

  # set configuration value
  if (defined $arg) {
    if ($arg ne 'file' and $arg ne 'memory') {
      printf STDERR "%s. ERROR: unknown argument token: %s\n", (caller(0))[3], $arg||"''";
      exit 1;
    }

    # final sync in old configuration
    if ($this->SyncValid()) { $this->Sync() }

    # reconfigure
    $this->{SyncMaster} = $arg;
  }

  return $this->{SyncMaster};
}


# read/set decision mode for synchronization
#
# INTERFACE
# - argument 1*: mode (for set mode)
# - return val:  mode
#
sub SyncMode {
  my ($this, $arg) = @_;

  # set configuration value
  if (defined $arg) {
    if ($arg !~ m/^(once|OnDestr|paranoid|std)$/) {
      printf STDERR "%s. ERROR: unknown argument token: %s\n", (caller(0))[3], $arg||"''";
      exit 1;
    }

    # final sync in old configuration
    if ($this->SyncValid()) { $this->Sync() }

    # reconfigure
    $this->{SyncMode} = $arg;
  }

  return $this->{SyncMode} ||= 'std';
}


# destroy object
#
sub DESTROY {
  my ($this) = @_;
  # final call for synchronization
  if ($this->SyncValid() and ($this->{SyncMaster}||'') eq 'memory') {
    $this->Sync();
  }
}


################################################################################
# diagnostics
################################################################################


# debug object
#
# INTERFACE
# - options:
#   -handle     output handle, default STDERR
#   -indent     indent by spaces (specify number), default: 0
#
# DEVELOPER'S NOTES
# - This method is safe against endless recursion since the only subfunction
#   that is called, $this->SyncValid(), is completely devoid of subfunction
#   calls.
#
sub SyncDebug {
  my ($this, %opt) = @_;
  my $hOut = $opt{-handle} || \*STDERR;
  my $indent = $opt{-indent};

  # output debugging protocol
  printf $hOut "%smemory instance: %s\n", ' 'x$indent, "$this";
  printf $hOut "%sfile instance: file %s, size %s\n", ' 'x$indent,
    $this->{SyncFile}||"''", -s($this->{SyncFile});
  printf $hOut "%ssync master: %s\n", ' 'x$indent, $this->{SyncMaster}||"''";
  printf $hOut "%ssync mode: %s\n", ' 'x$indent, $this->{SyncMode}||"''";
  printf $hOut "%ssync connected: %s\n", ' 'x$indent,
    $this->{SyncConnect}?'yes':'no';
  printf $hOut "%ssync valid: %s\n", ' 'x$indent,
    $this->SyncValid()?'yes':'no';
  printf $hOut "%ssync time stamps: memory %s, sync %s\n", ' 'x$indent,
    defined($this->{SyncTimeMemory})?$this->{SyncTimeMemory}:'_undef_',
    defined($this->{SyncTimeSync})?$this->{SyncTimeSync}:'_undef_';
}


# return intactness of synchronization mechanisms
#
# INTERFACE
# - return val: intactness (boolean)
#
# DEVELOPER'S NOTES
# - This method is safe against endless recursion since it does not chain
#   into any subfunctions.
#
sub SyncValid {
  my ($this) = @_;

  # need a file path
  unless (exists($this->{SyncFile}) and $this->{SyncFile}) { return 0 }

  # slave data structure
  # need readable file (or directory)
  if ($this->{SyncMaster} eq 'file') {
    unless (-r $this->{SyncFile}) { return 0 }
  }

  # slave file
  # need writable file (or directory)
  elsif ($this->{SyncMaster} eq 'memory') {
    unless (-w $this->{SyncFile}) { return 0 }
  }

  # undefined direction of data flow
  else { return 0 }

  # tests passed, return success
  return 1;
}


################################################################################
# functionality
################################################################################


# load from file
#
# INTERFACE
# - arguments:  *** NONE ***
# - options:    *** possibly ***
# - return val: success status (boolean)
#
# DESCRIPTION
# - this code is meant as a declaration only. Implement the action in your
#   derived class.
#
sub Load {
  printf STDERR "%s. FATAL ERROR: method not implemented\n", (caller(0))[3];
  exit 1;
}


# save to file
#
# INTERFACE
# - arguments:  *** NONE ***
# - options:    *** possibly ***
# - return val: success status (boolean)
#
# DESCRIPTION
# - this code is meant as a declaration only. Implement the action in your
#   derived class.
#
sub Save {
  die sprintf "%s. FATAL ERROR: method not implemented\n", (caller(0))[3];
}


# read/set connection status
#
# INTERFACE
# - argument 1*: connection status (for write mode, boolean)
# - return val:  connection status
#
sub SyncConnect {
  my ($this, $arg) = @_;
  my $debug = $this->{switch}{-debug};
  $debug and printf STDERR "%s. entered (%s)\n", (caller(0))[3],
    defined($arg)?'write '.$arg:'read';

  # set configuration value
  if (defined $arg) {
    if ($arg and !$this->SyncValid()) {
      printf STDERR "%s. WARNING: invalid sync object on connection - action cancelled\n", (caller(0))[3];
      return 0;
    }
    $this->{SyncConnect} = $arg;

    # init time stamps for sync testing:
    # not needed
    
    # call of Sync()
    # No! We will sync on demand only
  }

  return $this->{SyncConnect};
}


# notify memory touch
#
# INTERFACE
# - options:
#   -id         some object ID beside the memory address
#   -handle     output handle, default STDERR
#   -warn       output warnings about violation of slave/master status
# - return val: touch is allowed and has been registered (boolean)
#
# DESCRIPTION
# - This object (component, in a derived class) cannot auto-detect changes
#   to the memory instance. So, this methods provides a portal for notification
#   about such changes.
# - The function checks the SyncMaster setting in order to warn about memory
#   changes done in memory slave mode.
#
sub SyncMemTouch {
  my ($this, %opt) = @_;
  my $debug = $this->{switch}{-debug};
  $debug and printf STDERR "%s. entered\n", (caller(0))[3];

  # detect conflict
  my $bErr = ($this->SyncMaster() eq 'file');

  # output warning
  if ($bErr and ($opt{-warn}||=$opt{-handle})) {
    my $hOut = $opt{-handle} || \*STDERR;
    printf $hOut "%s%s%s."
      . " WARNING: data-write conflicts synchronization direction\n", (caller(0))[3],
      $opt{-id}?' ':'', $opt{-id};
  } else {
    $this->{SyncTimeMemory} = time();
  }

  # return name of file
  return ! $bErr;
}


# synchronize object with file
#
# INTERFACE
# - options:
#   -debug      [STD]
#   -force      enforce synchronization disregarding actual synchronization mode
#
# - return val: success status (boolean)
#
# DESCRIPTION
# - validity of synchronization parameters is checked in SyncValid()
# - prior to any sync, there needs to be a connection ($this->SyncConnect())
# - Here, we test the need of synchronization in accordance to SyncMode
#   parameter and SyncTime* variables.
#
# DEVELOPER'S NOTES
# - Avoid an endless recursion of $this->Sync() calls. This is achived by
#   registrating entry and exit of the method ($this->{SyncGoingOn}).
#   This strategy requires that there's only one single exit gate.
#
sub Sync {
  my ($this, %opt) = @_;
  my %lopt = $this->_LocalSwitch(%opt);
  my $debug = $lopt{-debug} || 0;
  $debug and printf STDERR "%s. entered\n", (caller(0))[3];
  $this->{SyncGoingOn}++ and return 1;
  my ($bSucc);

  # are we able to do a sync?
  if (0) { }
  elsif (! $this->SyncValid()) { $bSucc = 0 }
  elsif (! $this->{SyncConnect}) { $bSucc = 0 }

  # sync needed?
  elsif (($this->{SyncMode}||'') eq 'once' and $this->{SyncTimeSync}) { $bSucc = 1 }

  # update slave data structure
  elsif (($this->{SyncMaster}||'') eq 'file') {
    if (
      $lopt{-force} or
      ($this->{SyncMode}||'') eq 'paranoid' or 
      (my $ftime=&ftime($this->{SyncFile})) > ($this->{SyncTimeSync}||0)
    ) {
      if ($bSucc = $this->Load()) {
        $debug and printf STDERR "%s. file->memory sync successful\n", (caller(0))[3];
        $this->{SyncTimeSync} = $ftime;
      }
    } else {
      $bSucc = 1;
    }
  }

  # update slave file
  else {
    if (
      $lopt{-force} or
      ($this->{SyncMode}||'') eq 'paranoid' or 
      ($this->{SyncTimeMemory}||0) > ($this->{SyncTimeSync}||0)
    ) {
      if ($bSucc = $this->Save()) {
        $this->{SyncTimeSync} = time() - 1;
        $this->{SyncTimeMemory} = &Min($this->{SyncTimeMemory},$this->{SyncTimeSync});
      }
    } else {
      $bSucc = 1;
    }
  }

  $this->{SyncGoingOn} = 0;
  return $bSucc||0;
}


1;
# $Id: DataFileSync.pm,v 1.16 2018/06/05 18:02:56 szafrans Exp $
