--- /dev/null
+package OpenILS::Utils::Cronscript;
+
+# ---------------------------------------------------------------
+# Copyright (C) 2010 Equinox Software, Inc
+# Author: Joe Atzberger <jatzberger@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# ---------------------------------------------------------------
+
+# The purpose of this module is to consolidate the common aspects
+# of various cron tasks that all need the same things:
+# ~ non-duplicative processing, i.e. lockfiles and lockfile checking
+# ~ opensrf_core.xml file location
+# ~ common options like help and debug
+
+use strict;
+use warnings;
+
+use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
+use OpenSRF::System;
+use OpenSRF::AppSession;
+use OpenSRF::Utils::JSON;
+use OpenSRF::EX qw(:try);
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Utils::Lockfile;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+
+use File::Basename qw/fileparse/;
+
+use Data::Dumper;
+use Carp;
+
+our @extra_opts = ( # additional keys are stored here
+ # 'addlopt'
+);
+
+our $debug = 0;
+
+sub _default_self {
+ return {
+ # opts => {},
+ # opts_clean => {},
+ # default_opts_clean => {},
+ default_opts => {
+ 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
+ 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
+ 'debug' => 0,
+ 'verbose+' => 0,
+ 'help' => 0,
+ # 'internal_var' => 'XYZ',
+ },
+ # lockfile => undef,
+ # session => undef,
+ # bootstrapped => 0,
+ # got_options => 0,
+ auto_get_options_4_bootstrap => 1,
+ };
+}
+
+sub is_clean {
+ my $key = shift or return 1;
+ $key =~ /[=:].*$/ and return 0;
+ $key =~ /[+!]$/ and return 0;
+ return 1;
+}
+
+sub clean {
+ my $key = shift or return;
+ $key =~ s/[=:].*$//;
+ $key =~ s/[+!]$//;
+ return $key;
+}
+
+sub fuzzykey { # when you know the hash you want from, but not the exact key
+ my $self = shift or return;
+ my $key = shift or return;
+ my $target = @_ ? shift : 'opts_clean';
+ foreach (map {clean($_)} keys %{$self->{default_opts}}) { # TODO: cache
+ $key eq $_ and return $self->{$target}->{$_};
+ }
+}
+
+# MyGetOptions
+# A wrapper around GetOptions
+# {opts} does two things for GetOptions (see Getopt::Long)
+# (1) maps command-line options to the *other* variables where values are stored (in opts_clean)
+# (2) provides hashspace for the rest of the arbitrary options from the command-line
+#
+# TODO: allow more options to be passed here, maybe mimic Getopt::Long::GetOptions style
+#
+# If an arrayref argument is passed, then @ARGV will NOT be touched.
+# Instead, the array will be passed to GetOptionsFromArray.
+#
+
+sub MyGetOptions {
+ my $self = shift;
+ my $arrayref = @_ ? shift : undef;
+ if ($arrayref and ref($arrayref) ne 'ARRAY') {
+ carp "MyGetOptions argument is not an array ref. Expect GetOptionsFromArray to explode";
+ }
+ $self->{got_options} and carp "MyGetOptions called after options were already retrieved previously";
+ my @keys = sort {is_clean($b) <=> is_clean($a)} keys %{$self->{default_opts}};
+ $debug and print "KEYS: ", join(", ", @keys), "\n";
+ foreach (@keys) {
+ my $clean = clean($_);
+ my $place = $self->{default_opts_clean}->{$clean};
+ $self->{opts_clean}->{$clean} = $place; # prepopulate default
+ # $self->{opts}->{$_} = $self->{opts_clean}->{$clean}; # pointer for GetOptions
+ $self->{opts}->{$_} = sub {
+ my $opt = shift;
+ my $val = shift;
+ ref ( $self->{opts_clean}->{$opt} ) and ref($self->{opts_clean}->{$opt}) eq 'SCALAR'
+ and ${$self->{opts_clean}->{$opt}} = $val; # set the referent's value
+ $self->{opts_clean}->{$opt} = $val; # burn the map, stick the value there
+ }; # pointer for GetOptions
+ }
+ $arrayref ? GetOptionsFromArray($arrayref, $self->{opts}, @keys)
+ : GetOptions( $self->{opts}, @keys) ;
+
+ foreach (@keys) {
+ delete $self->{opts}->{$_}; # now remove the mappings from (1) so we just have (2)
+ }
+ $self->clean_mirror('opts'); # populate clean_opts w/ cleaned versions of (2), plus everything else
+
+ print $self->help() and exit if $self->{opts_clean}->{help};
+ $self->new_lockfile();
+ $self->{got_options}++;
+ return wantarray ? %{$self->{opts_clean}} : $self->{opts_clean};
+}
+
+sub new_lockfile {
+ my $self = shift;
+ $debug and $OpenILS::Utils::Lockfile::debug = $debug;
+ unless ($self->{opts_clean}->{nolockfile} || $self->{default_opts_clean}->{nolockfile}) {
+ $self->{lockfile_obj} = OpenILS::Utils::Lockfile->new($self->first_defined('lock-file'));
+ $self->{lockfile} = $self->{lockfile_obj}->filename;
+ }
+}
+
+sub first_defined {
+ my $self = shift;
+ my $key = shift or return;
+ foreach (qw(opts_clean opts default_opts_clean default_opts)) {
+ defined $self->{$_}->{$key} and return $self->{$_}->{$key};
+ }
+ return;
+}
+
+sub clean_mirror {
+ my $self = shift;
+ my $dirty = @_ ? shift : 'default_opts';
+ foreach (keys %{$self->{$dirty}}) {
+ defined $self->{$dirty}->{$_} or next;
+ $self->{$dirty . '_clean'}->{clean($_)} = $self->{$dirty}->{$_};
+ }
+}
+
+sub new {
+ my $class = shift;
+ my $self = _default_self;
+ bless ($self, $class);
+ $self->init(@_);
+ $debug and print "new ", __PACKAGE__, " obj: ", Dumper($self);
+ return $self;
+}
+
+sub add_and_purge {
+ my $self = shift;
+ my $key = shift;
+ my $val = shift;
+ my $clean = clean($key);
+ my @others = grep {/$clean/ and $_ ne $key} keys %{$self->{default_opts}};
+ unless (@others) {
+ $debug and print "unique key $key => $val\n";
+ $self->{default_opts}->{$key} = $val; # no purge, just add
+ return;
+ }
+ foreach (@others) {
+ $debug and print "variant of $key => $_\n";
+ if ($key ne $clean) { # if it is a dirtier key, delete the clean one
+ delete $self->{default_opts}->{$_};
+ $self->{default_opts}->{$key} = $val;
+ } else { # else update the dirty one
+ $self->{default_opts}->{$_} = $val;
+ }
+ }
+}
+
+sub init { # not INIT
+ my $self = shift;
+ my $opts = @_ ? shift : {}; # user can specify more default options to constructor
+# TODO: check $opts is hashref; then check verbose/debug first. maybe check negations e.g. "no-verbose" ?
+ @extra_opts = keys %$opts;
+ foreach (@extra_opts) { # add any other keys w/ default values
+ $debug and print "init() adding option $_, default value: $opts->{$_}\n";
+ $self->add_and_purge($_, $opts->{$_});
+ }
+ $self->clean_mirror;
+ return $self;
+}
+
+sub usage {
+ # my $self = shift;
+ return "\nUSAGE: $0 [OPTIONS]";
+}
+
+sub options_help {
+ my $self = shift;
+ my $chunk = @_ ? shift : '';
+ return <<HELP
+
+COMMON OPTIONS:
+ --osrf-config </path/to/config_file> Default: $self->{default_opts_clean}->{'osrf-config'}
+ Specify OpenSRF core config file.
+
+ --lock-file </path/to/file_name> Default: $self->{default_opts_clean}->{'lock-file'}
+ Specify lock file.
+
+HELP
+ . $chunk . <<HELP;
+ --debug Print server responses to STDOUT for debugging
+ --verbose Set verbosity
+ --help Show this help message
+HELP
+}
+
+sub help {
+ my $self = shift;
+ return $self->usage() . "\n" . $self->options_help(@_) . $self->example();
+}
+
+sub example {
+ return "\n\nEXAMPLES:\n\n $0 --osrf-config /my/other/opensrf_core.xml\n";
+}
+
+# the proper order is: MyGetOptions, bootstrap, session.
+# But the latter subs will check to see if they need to call the preceeding one(s).
+
+sub session {
+ my $self = shift or return;
+ $self->{bootstrapped} or $self->bootstrap();
+ @_ or croak "session() called without required argument (app_name, e.g. 'open-ils.acq')";
+ return ($self->{session} ||= OpenSRF::AppSession->create(@_));
+}
+
+sub bootstrap {
+ my $self = shift or return;
+ if ($self->{auto_get_options_4_bootstrap} and not $self->{got_options}) {
+ $debug and print "Automatically calling MyGetOptions before bootstrap\n";
+ $self->MyGetOptions();
+ }
+ try {
+ $debug and print "bootstrap lock-file : ", $self->first_defined('lock-file'), "\n";
+ $debug and print "bootstrap osrf-config: ", $self->first_defined('osrf-config'), "\n";
+ OpenSRF::System->bootstrap_client(config_file => $self->first_defined('osrf-config'));
+ Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
+ $self->{bootstrapped} = 1;
+ } otherwise {
+ $self->{bootstrapped} = 0;
+ warn shift;
+ };
+}
+
+sub editor_init {
+ my $self = shift or return;
+ OpenILS::Utils::CStoreEditor::init(); # no return value to check
+ $self->{editor_inited} = 1;
+}
+
+sub editor {
+ my $self = shift or return;
+ $self->{bootstrapped} or $self->bootstrap();
+ $self->{editor_inited} or $self->editor_init();
+ return new_editor(@_);
+}
+
+1;
+__END__
+
+=pod
+
+=head1 NAME
+
+OpenILS::Utils::Cronscript - Consolidated options handling for any script (not just cron, really)
+
+=head1 SYNOPSIS
+
+ use OpenILS::Utils::Cronscript;
+
+ my %defaults = (
+ 'min=i' => 0, # keys are Getopt::Long style options
+ 'max=i' => 999, # values are default values
+ 'user=s' => 'admin',
+ 'password=s' => '',
+ 'nolockfile' => 1,
+ };
+
+ my $core = OpenILS::Utils::Cronscript->new(\%defaults);
+ my $opts = $core->MyGetOptions(); # options now in, e.g.: $opts->{max}
+ $core->bootstrap;
+
+Or if you don't need any additional options and just want to get a session going:
+
+ use OpenILS::Utils::Cronscript;
+ my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
+
+=head1 DESCRIPTION
+
+There are a few main problems when writing a new script for Evergreen.
+
+=head2 Initialization
+
+The runtime
+environment for the application requires a lot of initialization, but during normal operation it
+has already occured (when Evergreen was started). So most of the EG code never has to deal with
+this problem, but standalone scripts do. The timing and sequence of requisite events is important and not obvious.
+
+=head2 Common Options, Consistent Options
+
+We need several common options for each script that accesses the database or
+uses EG data objects and methods. Logically, these options often deal with initialization. They
+should take the B<exact> same form(s) for each script and should not be
+dependent on the local author to copy and paste them from some reference source. We really don't want to encourage (let alone force)
+admins to use C<--config>, C<--osrf-confg>, C<-c>, and C<@ARGV[2]> for the same purpose in different scripts, with different
+default handling, help descriptions and error messages (or lack thereof).
+
+This suggests broader problem of UI consistency and uniformity, also partially addressed by this module.
+
+=head2 Lockfiles
+
+A lockfile is necessary for a script that wants to prevent possible simultaneous execution. For example, consider a script
+that is scheduled to run frequently, but that experiences occasional high load: you wouldn't want crontab to start running
+it again if the first instance had not yet finished.
+
+But the code for creating, writing to, checking for, reading and cleaning up a lockfile for the script bloats what might otherwise be a terse
+method call. Conscript handles lockfile generation and removal automatically.
+
+=head1 OPTIONS
+
+The common options (and default values) are:
+
+ 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
+ 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
+ 'debug' => 0,
+ 'verbose+' => 0,
+ 'help' => 0,
+
+=head1 TODO
+
+More docs here.
+
+=head1 SEE ALSO
+
+ Getopt::Long
+ OpenILS::Utils::Lockfile
+ oils_header.pl
+
+=head1 AUTHOR
+
+Joe Atzberger <jatzberger@esilibrary.com>
+
+=cut
+