The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
=head1	NAME

Remedy::ARSTools - a perl wrapper to the ARSperl project, providing a simplified object interface with field
definition caching.




=head1	SYNOPSIS

	use Remedy::ARSTools;
	
	#create a new object with a new field definition data cache
	my $Remedy = new Remedy::ARSTools(
		Server		=> $server_host_or_ip,
		User		=> $username,
		Pass		=> $password,
		ConfigFile	=> $file_to_cache_field_definition_data,
		Schemas		=> [ 'list', 'of', 'schema names', 'to get', 'field data for' ]
	) || die ($Remedy::ARSTools::errstr);
	
	#create a ticket
	my $ticket_number = $Remedy->CreateTicket(
		Schema	=> $schema_name,
		Fields	=> { 
			'fieldName1' => "value1", 
			'fieldName2' => "value2,
			... etc ...
		}
	) || die $Remedy->{'errstr'};
	
	#merge ticket
	my $ticket_number = $Remedy->MergeTicket(
	        Schema           => $schema_name,
	        MergeCreateMode  => "Overwrite",
	        Fields	         => { 
			'fieldName1' => "value1", 
			'fieldName2' => "value2,
			... etc ...
		}
        ) || die $Remedy->{'errstr'};
	
	#modify a ticket
	$Remedy->ModifyTicket(
		Schema	=> $schema_name,
		Ticket	=> $ticket_number,
		Fields	=> {
			'fieldName1' => "value1", 
			'fieldName2' => "value2,
			... etc ...
		}
	) || die $Remedy->{'errstr'};
	
	#query for tickets
	$tickets = $Remedy->Query(
		Schema	=> $schema_name,
		QBE		=> $qbe_string,
		Fields	=> ['array', 'of', 'fieldNames', 'to', 'retrieve']
	) || die $Remedy->{'errstr'};
	
	#delete a ticket
	$Remedy->DeleteTicket(
		Schema	=> $schema_name,
		Ticket	=> $ticket_number
	) || die $Remedy->{'errstr'};
	
	$parsed_diary = $Remedy->ParseDiary(
		Diary		   => $raw_diary_data_from_database,
	) || die $Remedy->{'errstr'};
	
	$Remedy->Destroy();



=head1	OVERVIEW & DEPENDENCIES

First things first, you need ARSperl installed for this module to work. ARSperl is the perl interface to the 
Remedy C API, and provides all the "magic" of talking to your Remedy server. This module is a perl wrapper
that sits atop ARSperl. The purpose of this module is to provide a nice, simplified interface to ARSperl that 
is independent of the particular version of ARSperl and the Remedy C API that you have installed.

You will need the following items to be installed prior to attempting to use this module:

=over

=item	Remedy C API

This comes as part of your Remedy server installation. This API is proprietary, and owned by the
Remedy corporation (or BMC, or Peregrin or whom ever owns them this week). You can usually find
this under the 'api' directory under the remedy installation directory on your remedy server. 
The Remedy C API is required by the ARSperl installation.

=item	ARSperl

as mentioned earlier, this is the perl interface to the Remedy C API. You can download ARSperl 
from your local CPAN mirror, or also from the sourceforge project page:

	http://sourceforge.net/projects/arsperl/
	
=item	Data::DumpXML

this perl module is available from your local CPAN mirror. It is used to serialize field definition
data into a configuration file.

=back




=head1	A NOTE ON FIELD DEFINITION DATA

Remedy assigns a unique 'field_id' to each field in a schema. In order to do pretty much anything with 
that field in the Remedy API, you must know the field_id rather than the name. For instance 'entry_id'
is typically field_id '1', however it gets a lot more complicated from there. Additionally, Remedy 
implements fields with enumerated values in a unique way, assigning an integer to each enumerated value
starting at 0. For instance, 'Status' = "New" = 0. One must also know the enum value corresponding to the
'human readable' value when performing operations using the API. 

This module attempts to hide all of that, allowing you to reference fields directly by name, and
enumerated field values by their 'human readable' (string) value (rather than by integer). However, to do so, 
the module needs to maintain a mapping of field id's and enumerated values. The mapping can be loaded from
the remedy server when you create a Remedy::ARSTools object, however, this is a rather time-consuming 
task, and is also network intensive. As an alternative, you can specify a special file in which the object
will store field definition data. This file acts as a field definition data cache, and it's contents are 
automatically updated.

Use of an external file in which to cache field definition data is highly recommended for speed improvments,
but is not completely necessary. The 'penalty' for not using the file, is that it takes much longer to 
instantiate new objects.




=head1	new

This is, of course, the object constructor. Upon failure, the method will return the undef value, and an error
message will be written to $Remedy::ARSTools::errstr. At object creation, the field definition data is loaded
either directly from the remedy server, or from the provided config file.

=head2	syntax

	my $Remedy = new Remedy::ARSTools([ options ]) || die $Remedy::ARSTools::errstr;
	
=head2	options

the following options are accepted by the new() function:

=over

=item	Server	(required)

this is the hostname or ip address of the remedy server to which access is desired.

=item	User	(required)

the 'Login Name' of the Remedy account to be used for access to 'Server'

=item	Pass	(required)

the password for 'User'

=item	ConfigFile

this is the full path and filename of the file in which field definition data should be cached (and 
which may already contain field definition data).

=item	LoginOverride

if a non-zero value is specified for this option, the function will not attempt to login to the 
remedy server until a function requiring it is called.

=item	Port

if specified, will instruct the C API to communicate with the Remedy server only on the specified TCP port.

=item	RPCNumber

if specified, will instruct the C API to communicate with the Remedy server using only the specified RPC
port (note only supported where ARSPerl > 8.001 is installed, also note, RPCNumber and Port are mutually
exclusive).

=item	Language

The language the user is using. If not specified, the default will be used.

=item	AuthString

It's here because it's in ARSPerl, and it's in ARSperl because it's in the C API. It "has something to do
with the Windows Domain", according to the ARSperl documentation. You can specify it here, and it'll be passed
on to ARSperl, if you know what to do with it.

=item	ReloadConfigOK

if 0 or the undef value are supplied on this argument, the module will not attempt to update cached field
definition data in the specified config, if it is found to be out of date.

=item	GenerateConfig

if 0 or the undef value are supplied on this argument, an error is generated if the specified ConfigFile
does not already contain field definition data. The specified ConfigFile will not be created if it does
not already exist.

=item	TruncateOK

If a non-zero value is specified on this argument, functions which write data into remedy will silently 
truncate field data values if they are too long to fit in their specified fields. This is a short cut to 
setting this option individually on every function call.

=back




=head1	LoadARSConfig

This function loads field definition data from the 'ConfigFile' specified in the object, or directly
from the Remedy server (if 'ConfigFile' dosen't exist, or the internal 'staleConfig' flag is set). 

Normally, this function is called only internally, but it can be used externally, to force an object
to reload it's field definition data.

=head2	syntax

	$Remedy->LoadARSConfig() || die $Remedy->{'errstr'};




=head1	ARSLogin

This function connects to the remedy server specified by the 'Server' in the object, obtaining a 
"control token" from the remedy server. If the object is already logged into the Remedy server, 
this function will return without doing anything. If the object's internal 'staleLogin' flag is set
true, or if the object is not yet connected to the Remedy server (such as when 'loginOverride' is specified
at object instantiation), the function will connect.

=head2	syntax

	$Remedy->ARSlogin() || die $Remedy->{'errstr'};




=head1	Destroy

This is the object destructor. This function releases the "control token" back to the Remedy server, 
clearing the user's session. This also completely destroys the object.

=head2	syntax

	$Remedy->Destroy();



=head1	CheckFields

This function checks a hash containing field name and value pairs against field definition data.
If a field value is too long, it is truncated (if the object's TruncateOK is set), otherwise an 
error is returned. Also string values provided for enum fields are converted to their integer values.

This function is unique, in that if no errors are found, the undef value is returned, with the string "ok"
on the object's errstr. If errors are found a string containing a concatenation of all errors found in
the field list is returned. If a more serious error is encountered (not relating to field values), then
the undef value is returned with a string other than "ok" on the object's errstr.

This is most definitely called internally, though it can be useful externally for data validation. 

=head2	syntax

	my $errors = $Remedy->CheckFields( [ options ] ) || do {
		die $Remedy->{'errstr'} if ($remedy->{'errstr'} ne "ok");
	};
	
	if ($errors !~/^\s*/){ print $errors, "\n"; }
	
=head2	options

the CheckFields function accepts the following options

=over

=item	Schema		(required)

the name of the schema in which the fields that values should be checked for exist.

=item	Fields		(required)

a hash reference in the form of { 'field_name' => $value  ... }, where each 'field_name' refers to a field
in 'Schema', and each $value represents a value for the field.

NOTE: the referenced hash will be modified (values truncated, or strings translated to integers for enum fields)

=back




=head1	CreateTicket

Create a new record in the specified Schema containing the specified field values.

=head2	syntax

	my $ticket_number = $Remedy->CreateTicket( [ options ] ) || die $Remedy->{'errstr'};

=head2	options

the following options are accepted by the CreateTicket function

=over

=item	Schema

the name of the schema in which the record should be created

=item	Fields

a hash reference in the form of { 'field_name' => $value ... }, where each 'field_name' is the name of a
field in 'Schema' and each $value is a value to place in that field.

=back




=head1	ModifyTicket

Change the specified field values in the specified record, in the specified Schema

=head2	syntax

	$Remedy->ModifyTicket( [ options ] ) || die $Remedy->{'errstr'};

=head2	options

the following options are accepted by the ModifyTicket function:

=over

=item	Ticket

the 'ticket number' (or 'entry id', or 'record number' ... field id number 1, that is) of the record
that we wish to modify.

=item	Schema

the name of the schema in which 'Ticket' exists

=item	Fields

a hash reference in the form of { 'field_name' => $value ... }, where each 'field_name' is the name of a
field in 'Ticket' and $value is the value to set on that field.

=back




=head1	DeleteTicket

Remove the specified record from the specified Schema. Obviously, this will fail if the 'User' specified
at instantiation, does not have administrator permissions.

=head2	syntax

	$Remedy->DeleteTicket( [ options ] ) || die $Remedy->{'errstr'};
	
=head2	options

=over

=item	Ticket

the 'ticket number' (or 'entry id', or 'record number' ... field id number 1, that is) of the record
that we wish to delete.

=item	Schema

the name of the schema in which 'Ticket' exists

=back




=head1	Query

Return selected field values from Tickets matching the specified query string in the specified Schema. 
It should be noted that having external processes query through the ARS API presents a lot of overhead
on the server, is slower than a direct SQL query to the underlying database, and at least in my opinion, 
should be avoided. However, If you're here, I'll presume you have your reasons ;-).

Data is returned as an array reference. Each element of the array is a hash reference, representing a ticket 
which matched the specified query string. The hash reference is in the form of { 'field_name' => $value ... }, 
where each 'field_name' is the name of a field in the ticket and $value is the value for that field.

=head2	syntax

	my $tickets = $Remedy->Query( [ options ] ) || die $Remedy->{'errstr'};
	
=head2	options

the Query function accepts the following arguments:

=over

=item	Schema

the name of the schema that you want to return matching records from

=item	QBE

this is the "Query By Example" string, or 'query string' or "that thing you type in the 'Search Criteria'
line when you click the 'Advanced' button in the client". You know what I'm talking about probably.
Just remember, it's not exactly the same thing as an SQL 'where' clause.

=item	Fields

An array contianing the list of field names corresponding to selected field values we'd like returned from
matching records in 'Schema'. You may find it helpful to build the array reference inline with the 
function call like so:
 
	Fields => ['field1','field2','field3']

=back




=head1	ParseDiary

This is one of those routines that dosen't seem to fit in anywhere perfectly. I included it in 
Remedy::ARSTools, because ... well, it's dern handy, and this seemed to be as good of a place as any for it.
If you took my advice from the notes on 'Query' and decided to do your queries directly from the
database used by your remedy server, then you might run into some problems with diaries. Remedy stores
the diary as one big ol' text field. Which is a problem, because as you are probably aware, the diary
has a timestamp and user associated with each entry. So what you get when you select a diary field from
your database, is each diary entry separated by some trash. This 'trash' is the username and timestamp.
So what this does is to parse a raw diary format into the same format as you would get if you had
queried a diary field via ARS (i.e. the same format as returned by ARS::getField.). That format is
an array reference. Each element in the array is, in turn, a hash reference. Each nested hash contains
three fields 'timestamp', 'user', and 'value'. Oh yeah, and the Array is sorted chronologically, with
the earliest entries first. Here's another look at what the data-structure looks like:

	\@DIARY = [
		{ 'timestamp' => $date, 'user' => $user, 'value' => $diary_entry }
		...
	];

=head2	syntax

	my $diary_entries = $Remedy->ParseDiary( [ options ] ) || die $Remedy->{'errstr'};

=head2	options

the following options are accepted by the ParseDiary function:

=over

=item	Diary

a big ol' text string from the database containing an unparsed diary

=item	ConvertDate

if a non-zero value is specified on this option, the timestamp field of each diary entry will
be converted from 'epoch' time to the local time zone set in the 'locale' on your computer.

=back

=head1 MergeTicket

Pretty much the same thing as CreateTicket, but with a merge transaction, allowing you all of the freedoms
and responsibilities that come with that (for heaven's sake: be careful, mmmkay?).

The returned value is one of two things. If you are in MergeMode = 'Create' or 'Error', this will return the
entry_id (i.e. "ticket number, aka "field id = 1") of the record you just merged. If you are in 
MergeMode = "Overwrite", AND the entry_id value you specified already exists this will contain the string
"overwritten" (you may now see my point about being careful).

=head2  syntax

        my $ticket_number = $Remedy->MergeTicket( [options] ) || die $Remedy->{'errstr'}

=head2  options

=over

=item   Schema

the name of the schema (i.e. "form") in which the record should be merged.

=item   Fields

a hash reference in the form of { 'field_name' => $value ... }, where each 'field_name' is the name of a
field in 'Schema' and each $value is a value to place in that field. Just like CreateTicket

=item   MergeCreateMode

this controls what happens when the record you want to merge has the same value for field_id = 1 as an existing
record. That is to say, what happens in the situation where you specify an existing ticket number on Fields. There
are three values:

=over

=item   Error

Throw an error and exit if there is an existing record in the spefified Schema with the same ticket number

=item   Create

Create a new record with a different ticket number. So, basically ignore your speficied field_id = 1 (ticket number) value
and create a new record with the rest of the field values, and return the new ticket number.

=item   Overwrite

This is the funny biznezz. If you specify this option, it will overwrite the existing ticket number with your values, and
the function will return "overwritten" instead of a ticket number. You have been warned :-).

=back

=item   AllowNullFields

if you specify a non-null value on this option, it will give the API permission to bypass required fields (excepting of course
the ARS system fields ... 'status', 'short description', yadda yadda).

=item   SkipFieldPatternCheck

if you specify a non-null value on this option, it will give the API permission to bypass field pattern checking (menus, etc).
Obviously, it will not let you set out of range enums and the like, but you'd never get that far anyhow ... this module would
throw a field check error before that, but I digress. Set this if you want to set goofy field values and get away with it.

=back
        
=head1	EXAMPLES

#create a new ticket in Users schema
my $ticket_number = $Remedy->CreateTicket(
	Schema		=> "User",
	Fields		=> {
		'Login Name'		=> "sbsqrpnts",
		'Password'			=> "tar-t4r-s4us3",
		'Group List'		=> "fryCooks jellyFishers",
		'Full Name'			=> "Squarepants, Sponge B.",
		'Email Address'		=> 'sbsqrpnts@krustykrab.com',
		'License Type'		=> "Fixed",
		'Assigned To'		=> "sbsqrpnts"	
	}
) || die ($Remedy->{'errstr'});

#query for tickets
my $tickets = $Remedy->Query(
	Schema	=> "Users",
	QBE 	=> "'Login Name' = \"sbsqrpnts\"",
	Fields	=> [ "Request ID", "Login Name" ]
) || die ($Remedy->{'errstr'});

#modify a ticket
$Remedy->modifyTicket(
	Ticket	=> $tickets->[0]->{'Request ID'},
	Schema	=> "User",
	Fields	=> { 'Full Name' => "SpongeBob Squarepants" }
) || die ($Remedy->{'errstr'});

#delete a ticket
$Remedy->DeleteTicket(
	Schema	=> "User",
	Ticket	=> $tickets->[0]->{'Request ID'}
) || die ($Remedy->{'errstr'});

#log out
$Remedy->Destroy();



=head1	AUTHOR

Andrew N. Hicox	<andrew@hicox.com>
Sutio BootyQuake
http://www.hicox.com




=head1	LICENSE

This module is released under the licensing terms of Perl itself.
http://www.arsperl.org/artistic.txt