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

use strict;
use warnings;

=head1 NAME

SeeAlso::Response - SeeAlso Simple Response

=cut

use JSON::XS qw(encode_json);
use Text::CSV;
use Data::Validate::URI qw(is_uri);
use Carp;

our $VERSION = "0.57";

=head1 DESCRIPTION

This class models a SeeAlso Simple Response which is practically the
same as am OpenSearch Suggestions Response.

=head1 METHODS

=head2 new ( [ $query [, $labels, $descriptions, $urls ] )

Creates a new L<SeeAlso::Response> object (this is the same as an
OpenSearch Suggestions Response object). The optional parameters
are passed to the set method, so this is equivalent:

  $r = SeeAlso::Response->new($query, $labels, $descriptions, $urls);
  $r = SeeAlso::Response->new->set($query, $labels, $descriptions, $urls);

To create a SeeAlso::Response from JSON use the fromJSON method.

=cut

sub new {
    my $this = shift;

    my $class = ref($this) || $this;
    my $self = bless {
        'query' => "",
        'labels' => [],
        'descriptions' => [],
        'urls' => []
    }, $class;

    $self->set(@_);

    return $self;
}

=head2 set ( [ $query [, $labels, $descriptions, $urls ] )

Set the query parameter or the full content of this response. If the
query parameter is an instance of L<SeeAlso::Identifier>, the return
of its C<normalized> method is used. This methods croaks if the passed
parameters do not fit to a SeeAlso response.

=cut

sub set {
    my ($self, $query, $labels, $descriptions, $urls) = @_;

    $self->query( $query );

    if (defined $labels) {
        croak ("bad arguments to SeeAlso::Response->new")
            unless ref($labels) eq "ARRAY"
                and defined $descriptions and ref($descriptions) eq "ARRAY"
                and defined $urls and ref($urls) eq "ARRAY";
        my $l = @{$labels};
        croak ("length of arguments to SeeAlso::Response->new differ")
            unless @{$descriptions} == $l and @{$urls} == $l;

        $self->{labels} = [];
        $self->{descriptions} = [];
        $self->{urls} = [];

        for (my $i=0; $i < @{$labels}; $i++) {
            $self->add($$labels[$i], $$descriptions[$i], $$urls[$i]);
        }
    }

    return $self;
}

=head2 add ( $label [, $description [, $uri ] ] )

Add an item to the result set. All parameters must be strings.
The URI is only partly checked for well-formedness, so it is 
recommended to use a specific URI class like C<URI> and pass 
a normalized version of the URI:

  $uri = URI->new( $uri_str )->canonical

Otherwise your SeeAlso response may be invalid. If you pass a 
non-empty URI without schema, this method will croak. If label,
description, and uri are all empty, nothing is added.

Returns the SeeAlso::Response object so you can chain method calls.

=cut

sub add {
    my ($self, $label, $description, $uri) = @_;

    if (defined $label) {
        croak("response label must be a string") if ref($label);
    } else {
        $label = "";
    }
    if (defined $description) {
        croak("response description must be a string") if ref($description);
    } else {
        $description = "";
    }
    if ( defined $uri && $uri ne "" ) {
        croak("irregular response URI") 
            unless $uri =~ /^[a-z][a-z0-9.+\-]*:/i;
    } else {
        $uri = "";
    }

    return $self unless $label ne "" or $description ne "" or $uri ne "";

    push @{ $self->{labels} }, $label;
    push @{ $self->{descriptions} }, $description;
    push @{ $self->{urls} }, $uri;

    return $self;
}

=head2 size ( )

Get the number of entries in this response.

=cut

sub size {
    my $self = shift;
    return scalar @{$self->{labels}};
}

=head2 get ( $index )

Get a specific triple of label, description, and url
(starting with index 0):

  ($label, $description, $url) = $response->get( $index )

=cut

sub get {
    my ($self, $index) = @_;
    return unless defined $index and $index >= 0 and $index < $self->size();
    
    my $label =  $self->{labels}->[$index];
    my $description = $self->{descriptions}->[$index];
    my $url =         $self->{urls}->[$index];

    return ($label, $description, $url);
}

=head2 query ( [ $query ] )

Get and/or set query parameter. If the query is a L<SeeAlso::Identifier>
it will be normalized, otherwise it will be converted to a string.

=cut

sub query {
    my $self = shift;
    if (@_) {
        my $query = shift;
        if (UNIVERSAL::isa( $query, 'SeeAlso::Identifier' )) {
            $query = $query->canonical; 
        }
        $self->{query} = defined $query ? "$query" : "";
    }
    return $self->{query};
}

=head2 toJSON ( [ $callback [, $json ] ] )

Return the response in JSON format and a non-mandatory callback wrapped
around. The method will croak if you supply a callback name that does
not match C<^[a-z][a-z0-9._\[\]]*$>.

The encoding is not changed, so please only feed response objects with
UTF-8 strings to get JSON in UTF-8. Optionally you can pass a L<JSON>
object to do JSON encoding of your choice.

=cut

sub toJSON {
    my ($self, $callback, $json) = @_;

    my $response = [
        $self->{query},
        $self->{labels},
        $self->{descriptions},
        $self->{urls}
    ];

    return _JSON( $response, $callback, $json );
}

=head2 fromJSON ( $jsonstring )

Set this response by parsing JSON format. Croaks if the JSON string 
does not fit SeeAlso response format. You can use this method as
as constructor or as method;

  my $response = SeeAlso::Response->fromJSON( $jsonstring );
  $response->fromJSON( $jsonstring )

=cut

sub fromJSON {
    my ($self, $jsonstring) = @_;
    my $json = JSON::XS->new->decode($jsonstring);

    croak("SeeAlso response format must be array of size 4")
        unless ref($json) eq "ARRAY" and @{$json} == 4;

    if (ref($self)) { # call as method
        $self->set(@{$json});
        return $self;
    } else { # call as constructor
        return SeeAlso::Response->new(@{$json});
    }
}

=head2 toCSV ( )

Returns the response in CSV format with one label, description, uri triple
per line. The response query is omitted. Please note that newlines in values
are allowed so better use a clever CSV parser!

=cut

sub toCSV {
    my ($self, $headers) = @_;
    my $csv = Text::CSV->new( { binary => 1, always_quote => 1 } );
    my @lines;
    for(my $i=0; $i<$self->size(); $i++) {
        my $status = $csv->combine ( $self->get($i) ); # TODO: handle error status
        push @lines, $csv->string();
    }    
    return join ("\n", @lines);
}

=head2 toRDF ( )

Returns the response as RDF triples in JSON/RDF structure.
Parts of the result that cannot be interpreted as valid RDF are omitted.

=cut

sub toRDF ( ) {
    my ($self) = @_;
    my $subject = $self->query();
    return { } unless is_uri($subject);
    my $values = { };

    for(my $i=0; $i<$self->size(); $i++) {
        my ($label, $predicate, $object) = $self->get($i);
        next unless is_uri($predicate); # TODO: use rdfs:label as default?

        if ($object) {
            next unless is_uri($object);
            $object = { "value" => $object, 'type' => 'uri' };
        } else {
            $object = { "value" => $label, 'type' => 'literal' };
        }

        if ($values->{$predicate}) {
            push @{ $values->{$predicate} }, $object;
        } else {
            $values->{$predicate} = [ $object ];
        }
    }

    return {
        $subject => $values
    };
}

=head2 toRDFJSON ( )

Returns the response as RDF triples in JSON/RDF format.

=cut

sub toRDFJSON {
    my ($self, $callback, $json) = @_;
    return _JSON( $self->toRDF(), $callback, $json );
}


=head2 toN3 ( )

Return the repsonse in RDF/N3 (including pretty print).

=cut

sub toN3 {
    my ($self) = @_;
    return "" unless $self->size();
    my $rdf = $self->toRDF();
    my ($subject, $values) = %$rdf;
    return "" unless $subject && %$values;
    my @lines;

    foreach my $predicate (keys %$values) {
        my @objects = @{$values->{$predicate}};
        if ($predicate eq 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
            $predicate = 'a';
        } elsif ($predicate eq 'http://www.w3.org/2002/07/owl#sameAs') {
            $predicate = '=';
        } else {
            $predicate =  "<$predicate>";
        }
        @objects = map {
            my $object = $_;
            if ($object->{type} eq 'uri') {
                '<' . $object->{value} . '>';
            } else {
                _escape( $object->{value} );
            }
        } @objects;
        if (@objects > 1) {  
            push @lines, (" $predicate\n    " . join(" ,\n    ", @objects) );
        } else {
            push @lines, " $predicate " . $objects[0];
        }
    }

    my $n3 = "<$subject>";
    if (@lines > 1) {
        return "$n3\n " . join(" ;\n ",@lines) . " .";
    } else {
        return $n3 . $lines[0] . " .";
    }
}

=head1 INTERNAL FUNCTIONS

=cut

my %ESCAPED = ( 
    "\t" => 't', 
    "\n" => 'n', 
    "\r" => 'r', 
    "\"" => '"',
    "\\" => '\\', 
);
 
=head2 _escape ( $string )

Escape a specific characters in a UTF-8 string for Turtle syntax / Notation 3

=cut

sub _escape {
    local $_ = $_[0];
    s/([\t\n\r\"\\])/\\$ESCAPED{$1}/sg;
    return '"' . $_  . '"';
}

=head2 _JSON ( $object [, $callback [, $JSON ] ] )

Encode an object as JSON string, possibly wrapped by callback method.

=cut

sub _JSON {
    my ($object, $callback, $JSON) = @_;

    croak ("Invalid callback name")
        if ( $callback and !($callback =~ /^[a-z][a-z0-9._\[\]]*$/i));

    # TODO: change this behaviour (no UTF-8) ?
    $JSON = JSON::XS->new->utf8(0) unless $JSON;

    my $jsonstring = $JSON->encode($object); 

    return $callback ? "$callback($jsonstring);" : $jsonstring;
}


1;

=head1 AUTHOR

Jakob Voss C<< <jakob.voss@gbv.de> >>

=head1 LICENSE

Copyright (C) 2007-2009 by Verbundzentrale Goettingen (VZG) and Jakob Voss

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself, either Perl version 5.8.8 or, at
your option, any later version of Perl 5 you may have available.