use 5.008;
use strict;
use warnings;
## skip Test::Tabs

package XML::Saxon::XSLT2;

use Carp;
use IO::Handle;
use Scalar::Util qw[blessed];
use XML::LibXML;

our $AUTHORITY = 'cpan:TOBYINK';
our $VERSION   = '0.010';

my $classpath;

BEGIN
{
	foreach my $path (qw(
		/usr/share/java/saxon9he.jar
		/usr/local/share/java/saxon9he.jar
		/usr/share/java/saxonb.jar
		/usr/local/share/java/saxonb.jar
		/usr/local/share/java/classes/saxon9he.jar
	)) {
		$classpath = $path if -e $path;
		last if defined $classpath;
	}

	require Inline;
}

sub import
{
	my ($class, @args) = @_;
	shift @args
		if @args && exists $args[0] && defined $args[0] && $args[0] =~ /^[\d\.\_]{1,10}$/;
	Inline->import( Java => 'DATA', CLASSPATH => $classpath, @args );
}

sub new
{
	my ($class, $xslt, $baseurl) = @_;
	$xslt = $class->_xml($xslt);
	
	if ($baseurl)
	{
		return bless { 'transformer' => XML::Saxon::XSLT2::Transformer->new($xslt, $baseurl) }, $class;
	}
	else
	{
		return bless { 'transformer' => XML::Saxon::XSLT2::Transformer->new($xslt) }, $class;
	}
}

sub parameters
{
	my ($self, %params) = @_;
	$self->{'transformer'}->paramClear();
	while (my ($k,$v) = each %params)
	{
		if (ref $v eq 'ARRAY')
		{
			(my $type, $v) = @$v;
			my $func = 'paramAdd' . {
				double    => 'Double',
				string    => 'String',
				long      => 'Long',
				'int'     => 'Long',
				integer   => 'Long',
				decimal   => 'Decimal',
				float     => 'Float',
				boolean   => 'Boolean',
				bool      => 'Boolean',
				qname     => 'QName',
				uri       => 'URI',
				date      => 'Date',
				datetime  => 'DateTime',
				}->{lc $type};
			croak "$type is not a supported type"
				if $func eq 'paramAdd';
			$self->{'transformer'}->$func($k, $v);
		}
		elsif (blessed($v) && $v->isa('DateTime'))
		{
			$self->{'transformer'}->paramAddDateTime($k, "$v");
		}	
		elsif (blessed($v) && $v->isa('Math::BigInt'))
		{
			$self->{'transformer'}->paramAddLong($k, $v->bstr);
		}
		elsif (blessed($v) && $v->isa('URI'))
		{
			$self->{'transformer'}->paramAddURI($k, "$v");
		}	
		else
		{
			$self->{'transformer'}->paramAddString($k, "$v");
		}
	}
	return $self;
}

sub transform
{
	my ($self, $doc, $type) = @_;
	$type = ($type =~ /^(text|html|xhtml|xml)$/i) ? (lc $type) : 'default';
	$doc  = $self->_xml($doc);
	return $self->{'transformer'}->transform($doc, $type);
}

sub transform_document
{
	my $self = shift;
	my $r    = $self->transform(@_);
	
	$self->{'parser'} ||= XML::LibXML->new;
	return $self->{'parser'}->parse_string($r);
}

sub messages
{
	my ($self) = @_;
	return @{ $self->{'transformer'}->messages };
}

sub media_type
{
	my ($self, $default) = @_;
	return $self->{'transformer'}->media_type || $default;
}

sub doctype_public
{
	my ($self, $default) = @_;
	return $self->{'transformer'}->doctype_public || $default;
}

sub doctype_system
{
	my ($self, $default) = @_;
	return $self->{'transformer'}->doctype_system || $default;
}

sub version
{
	my ($self, $default) = @_;
	return $self->{'transformer'}->version || $default;
}

sub encoding
{
	my ($self, $default) = @_;
	return $self->{'transformer'}->encoding || $default;
}

sub _xml
{
	my ($proto, $xml) = @_;
	
	if (blessed($xml) && $xml->isa('XML::LibXML::Document'))
	{
		return $xml->toString;
	}
	elsif (blessed($xml) && $xml->isa('IO::Handle'))
	{
		local $/;
		my $str = <$xml>;
		return $str;
	}
	elsif (ref $xml eq 'GLOB')
	{
		local $/;
		my $str = <$xml>;
		return $str;
	}
	
	return $xml;
}

1;

=head1 NAME

XML::Saxon::XSLT2 - process XSLT 2.0 using Saxon 9.x.

=head1 SYNOPSIS

 use XML::Saxon::XSLT2;
 
 # make sure to open filehandle in right encoding
 open(my $input, '<:encoding(UTF-8)', 'path/to/xml') or die $!;
 open(my $xslt, '<:encoding(UTF-8)', 'path/to/xslt') or die $!;
 
 my $trans  = XML::Saxon::XSLT2->new($xslt, $baseurl);
 my $output = $trans->transform($input);
 print $output;
 
 my $output2 = $trans->transform_document($input);
 my @paragraphs = $output2->getElementsByTagName('p');

=head1 DESCRIPTION

This module implements XSLT 1.0 and 2.0 using Saxon 9.x via L<Inline::Java>.

It expects Saxon to be installed in either '/usr/share/java/saxon9he.jar'
or '/usr/local/share/java/saxon9he.jar'. Future versions should be more
flexible. The saxon9he.jar file can be found at L<http://saxon.sourceforge.net/> -
just dowload the latest Java release of Saxon-HE 9.x, open the Zip archive,
extract saxon9he.jar and save it to one of the two directories above.

=head2 Import

 use XML::Saxon::XSLT2;

You can include additional parameters which will be passed straight on to
Inline::Java, like this:

 use XML::Saxon::XSLT2 EXTRA_JAVA_ARGS => '-Xmx256m';

The C<import> function I<must> be called. If you load this module without
importing it, it will not work. (Don't worry, it won't pollute your namespace.)

=head2 Constructor

=over 4

=item C<< XML::Saxon::XSLT2->new($xslt, [$baseurl]) >>

Creates a new transformation. $xslt may be a string, a file handle or an
L<XML::LibXML::Document>. $baseurl is an optional base URL for resolving
relative URL references in, for instance, E<lt>xsl:importE<gt> links.
Otherwise, the current directory is assumed to be the base. (For base URIs
which are filesystem directories, remember to include the trailing slash.)

=back

=head2 Methods

=over 4

=item C<< $trans->parameters($key=>$value, $key2=>$value2, ...) >>

Sets transformation parameters prior to running the transformation.

Each key is a parameter name.

Each value is the parameter value. This may be a scalar, in which case
it's treated as an xs:string; a L<DateTime> object, which is treated as
an xs:dateTime; a L<URI> object, xs:anyURI; a L<Math::BigInt>, xs:long;
or an arrayref where the first element is the type and the second the
value. For example:

 $trans->parameters(
    now             => DateTime->now,
    madrid_is_capital_of_spain => [ boolean => 1 ],
    price_of_fish   => [ decimal => '1.99' ],
    my_link         => URI->new('http://example.com/'),
    your_link       => [ uri => 'http://example.net/' ],
 );

The following types are supported via the arrayref notation: float, double,
long (alias int, integer), decimal, bool (alias boolean), string, qname, uri,
date, datetime. These are case-insensitive.

=item C<< $trans->transform($doc, [$output_method]) >>

Run a transformation, returning the output as a string.

$doc may be a string, a file handle or an L<XML::LibXML::Document>.

$output_method may be 'xml', 'xhtml', 'html' or 'text' to override
the XSLT output method; or 'default' to use the output method specified
in the XSLT file. 'default' is the default. In the current release,
'default' is broken. :-(

=item C<< $trans->transform_document($doc, [$output_method]) >>

As per <transform>, but returns the output as an L<XML::LibXML::Document>.

This method is slower than C<transform>.

=item C<< $trans->messages >>

Returns a list of string representations of messages output by
E<lt>xsl:messageE<gt> during the last transformation run.

=item C<< $trans->media_type($default) >>

Returns the output media type for the transformation.

If the transformation doesn't specify an output type, returns the default.

=item C<< $trans->doctype_public($default) >>

Returns the output DOCTYPE public identifier for the transformation.

If the transformation doesn't specify a doctype, returns the default.

=item C<< $trans->doctype_system($default) >>

Returns the output DOCTYPE system identifier for the transformation.

If the transformation doesn't specify a doctype, returns the default.

=item C<< $trans->version($default) >>

Returns the output XML version for the transformation.

If the transformation doesn't specify a version, returns the default.

=item C<< $trans->encoding($default) >>

Returns the output encoding for the transformation.

If the transformation doesn't specify an encoding, returns the default.

=back

=head1 BUGS

Please report any bugs to L<http://rt.cpan.org/>.

=head1 SEE ALSO

L<XML::LibXSLT> is probably more reliable in terms of easy installation on a
variety of platforms, and it allows you to define your own XSLT extension
functions. However, the libxslt library that it's based on only supports XSLT
1.0.

This module uses L<Inline::Java>.

L<http://saxon.sourceforge.net/>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT

Copyright 2010-2012, 2014 Toby Inkster

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

=cut

__DATA__
__Java__
import net.sf.saxon.s9api.*;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.XMLFilterImpl;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.XMLFilterImpl;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.OutputKeys;
import java.io.*;
import java.math.BigDecimal;
import java.net.URI;
import java.util.*;

public class Transformer
{
	private XsltExecutable xslt;
	private Processor proc;
	private HashMap<String, XdmAtomicValue> params;
	public List messagelist;

	public Transformer (String stylesheet)
		throws SaxonApiException
	{
		proc = new Processor(false);
		XsltCompiler comp = proc.newXsltCompiler();
		xslt = comp.compile(new StreamSource(new StringReader(stylesheet)));
		params = new HashMap<String, XdmAtomicValue>();
		messagelist = new ArrayList();
	}
	
	public Transformer (String stylesheet, String baseuri)
		throws SaxonApiException
	{
		proc = new Processor(false);
		XsltCompiler comp = proc.newXsltCompiler();
		xslt = comp.compile(new StreamSource(new StringReader(stylesheet), baseuri));
		params = new HashMap<String, XdmAtomicValue>();
		messagelist = new ArrayList();
	}
	
	public void paramAddString (String key, String value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddLong (String key, long value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddDecimal (String key, BigDecimal value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddFloat (String key, float value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddDouble (String key, double value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddBoolean (String key, boolean value)
	{
		params.put(key, new XdmAtomicValue(value));
	}

	public void paramAddURI (String key, String value)
		throws java.net.URISyntaxException
	{
		params.put(key, new XdmAtomicValue(new URI(value.toString())));
	}

	public void paramAddQName (String key, String value)
	{
		params.put(key, new XdmAtomicValue(new QName(value.toString())));
	}

	public void paramAddDate (String key, String value)
		throws SaxonApiException
	{
		ItemTypeFactory itf = new ItemTypeFactory(proc);
		ItemType dateType = itf.getAtomicType(new QName("http://www.w3.org/2001/XMLSchema", "date"));
		params.put(key, new XdmAtomicValue(value, dateType));
	}

	public void paramAddDateTime (String key, String value)
		throws SaxonApiException
	{
		ItemTypeFactory itf = new ItemTypeFactory(proc);
		ItemType dateTimeType = itf.getAtomicType(new QName("http://www.w3.org/2001/XMLSchema", "datetime"));
		params.put(key, new XdmAtomicValue(value, dateTimeType));
	}

	public XdmAtomicValue paramGet (String key)
	{
		return params.get(key);
	}

	public void paramRemove (String key)
	{
		params.remove(key);
	}
	
	public void paramClear ()
	{
		params.clear();
	}
	
	public Object[] messages ()
	{
		return messagelist.toArray();
	}
	
	public String media_type ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
	}

	public String doctype_public ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.DOCTYPE_PUBLIC);
	}

	public String doctype_system ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.DOCTYPE_SYSTEM);
	}

	public String method ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.METHOD);
	}

	public String version ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.VERSION);
	}

	public String standalone ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.STANDALONE);
	}

	public String encoding ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.ENCODING);
	}

	public String indent ()
	{
		return xslt.getUnderlyingCompiledStylesheet().getOutputProperties().getProperty(OutputKeys.INDENT);
	}

	public String transform (String doc, String method)
		throws SaxonApiException
	{
		XdmNode source = proc.newDocumentBuilder().build(
			new StreamSource(new StringReader(doc))
			);

		XsltTransformer trans = xslt.load();
		trans.setInitialContextNode(source);

		Serializer out = new Serializer();
		StringWriter sw = new StringWriter();
		out.setOutputWriter(sw);
		trans.setDestination(out);

		if (!method.equals("default"))
		{
			out.setOutputProperty(Serializer.Property.METHOD, method);
		}

		Iterator i = params.keySet().iterator();
		while (i.hasNext())
		{
			Object k = i.next();
			XdmAtomicValue v = params.get(k);
			
			trans.setParameter(new QName(k.toString()), v);
		}

		messagelist.clear();
		trans.setMessageListener(
			new MessageListener() {
				public void message(XdmNode content, boolean terminate, SourceLocator locator) {
					messagelist.add(content.toString());
				}
			}
		);
		
		trans.transform();
				
		return sw.toString();
	}
}