The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Webservice::InterMine::IDResolutionJob;

use strict;
use Moose;

use MooseX::Types::Moose qw/Num Str Bool ArrayRef HashRef Maybe/;
use Webservice::InterMine::Types qw/Service/;

use Time::HiRes qw/gettimeofday/;
use Carp qw(croak confess);

require HTTP::Request::Common;

=head1 NAME

Webservice::InterMine::IDResolutionJob

=head1 SYNOPSIS

    use strict;
    use Webservice::InterMine;
    use Data::Dumper;

    my $service = Webservice::InterMine->get_service('www.flymine.org/query');

    my $job = $service->resolve_ids(
        identifiers => [qw/eve zen r bib Mad h/],
        type => 'Gene',
        extra => 'D. melanogaster'
    );

    $job->poll until ($job->completed);

    print Dumper($job->results);

=head1 DESCRIPTION

ID Resolution jobs are asynchronous requests to a web service to resolve a set of
identifiers to the objects available in the services data-store. This object records the
request made and provides mechanisms for checking the status of the request and retrieving
the results when they become available.

=head1 ATTRIBUTES

=head2 service

isa: L<Webservice::InterMine::Service>
is: ro
required: true

The service this request was made to.

=cut

has service => (
    isa => Service,
    required => 1,
    is => 'ro'
);

=head2 identifiers

isa: Array of Str
is: ro
required: true

The identifiers to resolve.

=cut

has identifiers => (
    isa => ArrayRef[Str],
    is  => 'ro',
    required => 1
);

=head2 uid

isa: Str
is: ro

The unique identifier of this job on the server.

=cut

has uid => (
    isa => Str,
    is => 'ro',
    builder => '_init_uid'
);

sub _init_uid {
    my $self = shift;
    my $service = $self->service;
    my $resp = $service->post(
        $service->build_uri($service->root . '/ids'),
        'Content-Type' => 'application/json',
        'Content' => $service->encode($self->as_submission)
    );
    if ( $resp->is_error ) {
        my $error = eval { $service->decode($resp->content)->{error} };
        $error ||= $resp->content;
        croak sprintf "%s: %s", $resp->status_line, $error;
    }
    my $data = $service->decode($resp->content);
    unless ($data->{wasSuccessful}) {
        confess $data->{error};
    }
    return $data->{uid};
}

=head2 type

isa: Str
is: ro
required: true

The type of objects these identifiers are meant to resolve to (eg. Gene).

=cut 

has type => (
    isa => Str,
    is  => 'ro',
    required => 1
);

=head2 extra

isa: Str
is: ro
required: false

An optional extra value used to disambiguate the ID resolution, such as the organism name.

=cut

has extra => (
    isa => Maybe[Str],
    is  => 'ro',
);

=head2 caseSensitive

isa: Bool
is: ro
required: false
default: false

Whether or not the identifiers should be treated case-sensitively or not.

=head2 wildCards 

isa: Bool
is: ro
required: false
default: false

Whether or not to interpret '*'s in identifiers as wildcards.

=cut

has [qw/caseSensitive wildCards/] => (
    isa => Maybe[Bool],
    is => 'ro'
);

=head2 completed

isa: Bool
is: ro
required: false
default: false

whether or not this job has been completed yet.

=cut

has completed => (
    isa => Bool,
    is => 'ro',
    writer => '_set_completed',
    default => 0
);

=head2 times_polled

isa: Num
is: ro
required: false
init: 0

The number of times this job has polled for results.

=cut

has times_polled => (
    isa => Num,
    traits => ['Counter'],
    is => 'ro',
    default => 0,
    handles => {_polled => 'inc'}
);

=head2 last_poll

isa: Num
is: ro
required: false

The timestamp of the last poll.

=cut

has last_poll => (
    isa => Num,
    required => 0,
    is => 'ro',
    writer => '_polled_at'
);

=head2 results

The results of the job. Do not call for them before the job reports its completion.

=cut

has results => (
    is => 'ro',
    isa => HashRef,
    lazy_build => 1,
    builder => 'fetch_results'
);

=head1 METHODS

=head2 as_submission()

Get the data transmitted to the service to initialise the job.

=cut

sub as_submission {
    my $self = shift;
    return {
        identifiers   => $self->identifiers,
        type          => $self->type,
        extra         => ($self->extra || ''),
        caseSensitive => ($self->caseSensitive ? 'true' : 'false'),
        wildCards     => ($self->caseSensitive ? 'true' : 'false'),
    };
}

sub _register_poll {
    my $self = shift;
    $self->_polled();
    $self->_polled_at( gettimeofday() );
}

sub _backoff {
    my $self = shift;
    if (my $polls = $self->times_polled) {
        my $backoff = log $polls;
        my $elapsed = gettimeofday() - $self->last_poll();
        $backoff -= $elapsed;
        if ($backoff > 0) {
            sleep $backoff;
        }
    }
}

=head2 poll()

Check the status of the job on the server.

Returns true when the job is complete, and false if it is not ready yet. If the job has
resulted in an error on the server, that error message will be confessed here.

=cut

sub poll {
    my $self = shift;
    return 1 if $self->completed;
    $self->_backoff;
    my $data = $self->service->fetch_json('/ids/' . $self->uid . '/status');
    $self->_register_poll;
    my $status = $data->{status};
    if ($status eq 'ERROR') {
        confess($data->{message});
    } elsif ($status eq 'SUCCESS') {
        return $self->_set_completed(1);
    } else {
        return undef;
    }
}

=head2 fetch_results() 

Make a call to the server to fetch results for this job.

=cut

sub fetch_results {
    my $self = shift;
    my $data = $self->service->fetch_json('/ids/' . $self->uid . '/result');
    return $data->{results};
}

=head2 delete()

Delete this job from the server.

=cut

sub delete {
    my $self = shift;
    my $uri = $self->service->build_uri($self->service->root . '/ids/' . $self->uid);
    my $resp = $self->service->agent->request(HTTP::Request::Common::DELETE($uri));
    if ($resp->is_error) {
        my $error = eval {$self->decode($resp->content)->{error}}
            || $resp->status_line . $resp->content;
        confess $error;
    }
}

__PACKAGE__->meta->make_immutable;
no Moose;

1;

__END__

=head1 AUTHOR

Alex Kalderimis C<dev@intermine.org>

=head1 BUGS

Please report any bugs or feature requests to C<dev@intermine.org>.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Webservice::InterMine::IDResolutionJob

You can also look for information at:

=over 4

=item * InterMine

L<http://www.intermine.org>

=item * Documentation

L<http://intermine.org/wiki/PerlWebServiceAPI>

=back

=head1 COPYRIGHT AND LICENSE

Copyright 2006 - 2013 FlyMine, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.