package PROP::Exception;

use strict;
use overload ('""' => \&stringify);

sub new {
    my ($class, $msg, $file, $line) = @_;
    my $self = bless({}, $class);

    $msg ||= 'Died';

    unless($file and $line) {
	(undef, $file, $line) = caller();
    }

    $self->{-msg} = format_text("...$msg in file $file at line $line", 80, 4);

    return $self;
}

sub throw {
    my ($self) = @_;
    die $self;
}

sub clone {
    my ($self) = @_;
    my $twin = bless({}, ref($self));

    $twin->{-msg} = $self->{-msg};

    return $twin;
}

sub PROPAGATE {
    my ($self, @args) = @_;

    my ($pkg, $file, $line, $msg);
    my $twin = $self->clone();

    if(scalar(@args) == 1) {
	$msg = $args[0];
	($pkg, $file, $line) = caller();
    }
    elsif(scalar(@args) == 2) {
	$msg = 'propagated';
	($file, $line) = @args;
    }
    else {
	($pkg, $file, $line) = caller();
	die "inappropriate args given to PROPAGATE at $file $line";
    }

    $twin->{-msg} .= "\n" . format_text("...$msg in file $file at line $line", 80, 4);

    return $twin;
}

sub format_text {
    my ($input, $max_length, $indent) = @_;

    my $line_length = $indent;
    my $output = ' ' x $indent;
    my @chunks = split /(\s+)/, $input;

    while(@chunks) {
	my $chunk = shift(@chunks);

	if($line_length == 0 or $line_length + length($chunk) < $max_length) {
	    $line_length += length($chunk);
	    $output .= $chunk;
	}
	else {
	    unless($chunk =~ /^\s+$/) {
		$output .= "\n" . (' ' x $indent);
		$line_length = length($chunk) + $indent;
		$output .= $chunk;
	    }
	}
    }

    return $output;
}

sub stringify {
    my ($self) = @_;
    my ($pkg, $file, $line) = caller();
    return "\n" . ref($self) . "\n" . $self->{-msg} . "\n";
}

1;

=head1 Name

PROP::Exception

=head1 Description

PROP::Exception is the base class of all exceptions thrown
within the PROP framework.  It may itself be thrown, or it may
be subclassed to provide for different types of exceptions.  Among
other things (OK, there aren't really other things), this class
provides for elegant propagation of exceptions and clean formatting of
the resultant trace.

=head1 Synopsis

 foo();
 
 sub foo {
     eval {
 	bar();
     };
 
     die $@->PROPAGATE("and in turn foo fell") if $@;
 }
 
 sub bar {
     eval {
 	baz();
     };
 
     die if $@;
 }
 
 sub baz {
     die new PROP::Exception("baz is dead! long live baz!");
 }

__END__

PROP::Exception
    ...baz is dead! long live baz! in file ./foo.pl at line 26
    ...propagated in file ./foo.pl at line 22
    ...and in turn foo fell in file ./foo.pl at line 14

=head1 Usage

When in the course of execution your application runs up against an
exceptional condition, an exception of some sort should the thrown.
This entails nothing more than constructing a new exception object and
invoking die with it as the sole argument.  The exception object may
be constructed with a string argument, and it is recommended that this
string contain contextual information that will help either a human or
a program at a higher context ascertain what went wrong.

Not only might you find need for initiating an exception, but you may
also find yourself in the position of trapping an exception and
wishing to propagate it to a higher level, perhaps first adding some
contextual information to it.  If you wish merely to propagate the
exception, simply invoke die without any argument, and the exception
object will auto-magically have propagation information embedded in
it, including the file name and line number from whence you propagated
it.  If, instead, you desire to embed some kind of additional
contextual information, then invoke with the return value of an
invocation of the exceptions PROPGATE method, passing a string into
it, for example...

 die $@->PROPAGATE("we got hosed when x was equal to $x") if $@;

Now, eventually the exception might percolate up to a level at which
it is uncaught, in which case Perl will convert the exception to a
string value, print it, and then terminate.  You will first see a line
that tells you that there was an exception and what type it was.
Underneath this you will see one or more lines, each indented with a
tab, and each of which contains a the message from the corresponding
propagation step.  With this information you will hopefully be able to
ascertain what went wrong with your program with much greater ease
than had your program bottomed out and died without a hierarchy of
contextual information.

Of course, you might be able to handle an exception at some level,
averting program termination and allowing for continued execution.  In
this case, you might want to continue on silently as if nothing had
happened, or you might want to print the exception either to a console
or a log file.  The same message that you see for an uncaught
exception may be obtained by invoking the exception object's stringify
method, for example...

 print "we averted the following disaster...\n", $@->stringify();

It is conceivable that in a given situation you might receive
notification of one of several different types of exceptions (because
you might have created a bunch of different ones by subclassing
PROP::Exception).  In this case, you will want to figure out
which type it was and act appropriately.  For example...

 if($@) {
     if(ref($@) eq 'MyTolerableException') {
         # handle it and bravely soldier on
     }
     elsif(ref($@) eq 'MyIntolerableException') {
	 # propagate with some useful context info
	 die $@->PROPAGATE("we got hosed when x was equal to $x");
     }
     else {
	 # don't even know what kind of exception this is...
	 die;
     }
 }

In the first case, you caught a recognizable exception from which you
can recover.  In the second case you caught a recognizable exception
from which you cannot recover, and so you propagate it with some
useful information, in hopes that a higher context will be able to
deal with it, or at least glean some valuable information from the
failure.  In the last case, you caught an exception that you don't
even recognize, so you just blindly propagate it, letting the no
argument form of die do its magic.

=head1 Methods

=over

=item new

 die new PROP::Exception();
 die new PROP::Exception($message);
 die new PROP::Exception($message, $file, $line);

The "new" method constructs an exception object, and Perl's built-in
"die" method throws it.  Three forms of the constructor are available.
The no arguments form defaults the message to "Died", and the file and
line to the point at which the exception object was created.  The one
argument form defaults the file and line to the point at which the
exception object was created, allowing you to specify the message
yourself.  The three argument form allows you to fully specify the
exception yourself, which is useful if you wish to have the error
appear from the perspective of somewhere other than the point where
the exception object was created (for example, the subclass
PROP::Exception::IllegalArgument specifies the file and line to be the
point at which the subroutine that threw the exception was invoked,
aiding a developer in the quest to find the location of the invalid
subroutine invocation).

=item PROPAGATE

 die $@->PROPAGATE($message);
 die $@->PROPAGATE($file, $line);

The PROPAGATE method is used when you have caught an exception and
wish to throw it up to a higher level for further processing.  The one
argument form is used when you wish to include some additional
information in the exception, filling in file and line information
automatically from the point at which the exception was propagated.
The two argument form really only exists to allow proper functioning
of the "magical" zero argument form of Perl's built-in "die" which
translates to

 die $@->PROPAGATE(__FILE__, __LINE);

=item stringify

This method returns a pleasantly formatted string version of the
exception.  The format is as follow...  On the first line and flush to
the left is the name of the exception class.  On subsequent lines and
indented by four spaces is information about the exception.  Each
level of propagation starts with an ellipsis and may span multiple
lines, as lines are wrapped to avoid extending past eighty characters,
since exceeding the width of a terminal can make reading error
messages an eye-blurring experience.

=back

=head1 Author

Andrew Gibbs (awgibbs@awgibbs.com,andrew.gibbs@nist.gov)

=head1 Legalese

This software was developed at the National Institute of Standards and
Technology by employees of the Federal Government in the course of
their official duties. Pursuant to title 17 Section 105 of the United
States Code this software is not subject to copyright protection and
is in the public domain. PROP is an experimental system. NIST
assumes no responsibility whatsoever for its use by other parties, and
makes no guarantees, expressed or implied, about its quality,
reliability, or any other characteristic. We would appreciate
acknowledgement if the software is used.  This software can be
redistributed and/or modified freely provided that any derivative
works bear some notice that they are derived from it, and any modified
versions bear some notice that they have been modified.
