package Plack::Middleware::XSLT;
{
  $Plack::Middleware::XSLT::VERSION = '0.20002';
}
use strict;

# ABSTRACT: XSLT transformations with Plack

use parent 'Plack::Middleware';

use File::Spec;
use HTTP::Exception ();
use Plack::Response;
use Plack::Util::Accessor qw(cache path parser_options);
use Try::Tiny;
use XML::LibXML 1.62;
use XML::LibXSLT 1.62;

my ($parser, $xslt);

sub call {
    my ($self, $env) = @_;

    my $r     = $self->app->($env);
    my $style = $env->{'xslt.style'};

    return $r if !defined($style) || $style eq '';

    my $path = $self->path;
    $style = File::Spec->catfile($path, $style)
        if defined($path) && !File::Spec->file_name_is_absolute($style);

    my ($status, $headers, $body) = @$r;
    my $doc = $self->_parse_body($body);

    my ($output, $media_type, $encoding) = $self->_xform($style, $doc);

    my $res = Plack::Response->new($status, $headers, $output);
    $res->content_type("$media_type; charset=$encoding");
    $res->content_length(length($output));

    return $res->finalize();
}

sub _xform {
    my ($self, $style, $doc) = @_;

    if (!$xslt) {
        if ($self->cache) {
            require XML::LibXSLT::Cache;
            $xslt = XML::LibXSLT::Cache->new;
        }
        else {
            $xslt = XML::LibXSLT->new;
        }
    }

    my $stylesheet = $xslt->parse_stylesheet_file($style);

    my $result = try {
        $stylesheet->transform($doc) or die("XSLT transform failed: $!");
    }
    catch {
        for my $line (split(/\n/, $_)) {
            HTTP::Exception->throw($1) if $line =~ /^(\d\d\d)(?:\s|\z)/;
        }
        die($_);
    };

    my $output     = $stylesheet->output_as_bytes($result);
    my $media_type = $stylesheet->media_type();
    my $encoding   = $stylesheet->output_encoding();

    return ($output, $media_type, $encoding);
}

sub _parse_body {
    my ($self, $body) = @_;

    if (!$parser) {
        my $options = $self->parser_options;
        $parser = $options
                ? XML::LibXML->new($options)
                : XML::LibXML->new;
    }

    my $doc;

    if (ref($body) eq 'ARRAY') {
        my $xml = join('', @$body);

        $doc = $parser->parse_string($xml);
    }
    else {
        $doc = $parser->parse_fh($body);
    }

    return $doc;
}

sub _cache_hits {
    my $self = shift;

    return $xslt->cache_hits
        if $xslt && $xslt->isa('XML::LibXSLT::Cache');

    return 0;
}

1;



=pod

=head1 NAME

Plack::Middleware::XSLT - XSLT transformations with Plack

=head1 VERSION

version 0.20002

=head1 SYNOPSIS

    # in your .psgi

    enable 'XSLT';

    # in your app

    $env->{'xslt.style'} = 'stylesheet.xsl';

    return [ 200, $headers, [ $xml ] ];

=head1 DESCRIPTION

Plack::Middleware::XSLT converts XML response bodies to HTML, XML, or text
using XML::LibXSLT. The XSLT stylesheet is specified by the environment
variable 'xslt.style'. If this variable is undefined or empty, the response
is not altered. This rather crude mechanism might be enhanced in the future.

The Content-Type header is set according to xsl:output. Content-Length is
adjusted.

=head1 CONFIGURATION

=over 4

=item cache

    enable 'XSLT', cache => 1;

Enables caching of XSLT stylesheets. Defaults to false.

=item path

    enable 'XSLT', path => 'path/to/xsl/files';

Sets a path that will be prepended if xslt.style contains a relative path.
Defaults to the current directory.

=item parser_options

    enable 'XSLT', parser_options => \%options;

Options that will be passed to the XML parser when parsing the input
document. See L<XML::LibXML::Parser/"PARSER OPTIONS">.

=back

=head1 HTTP EXCEPTIONS

If the transform exits via C<<xsl:message terminate="yes">> and the
message contains a line starting with a three-digit HTTP response status
code, a corresponding L<HTTP::Exception> is thrown.

=head1 AUTHOR

Nick Wellnhofer <wellnhofer@aevum.de>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by Nick Wellnhofer.

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

=cut


__END__