Bug 14382: Non-ISO Date parameters generate empty reports.
[koha.git] / Koha / XSLT_Handler.pm
blob3b30ee852085d294c07b5345e4079cfb8fb61add
1 package Koha::XSLT_Handler;
3 # Copyright 2014 Rijksmuseum
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 =head1 NAME
22 Koha::XSLT_Handler - Facilitate use of XSLT transformations
24 =head1 SYNOPSIS
26 use Koha::XSLT_Handler;
27 my $xslt_engine = Koha::XSLT_Handler->new;
28 my $output = $xslt_engine->transform($xml, $xsltfilename);
29 my $err= $xslt_engine->err; # error number
30 my $errstr= $xslt_engine->errstr; # error message
31 $xslt_engine->refresh($xsltfilename);
33 =head1 DESCRIPTION
35 A XSLT handler object on top of LibXML and LibXSLT, allowing you to
36 run XSLT stylesheets repeatedly without loading them again.
37 Errors occurring during loading, parsing or transforming are reported
38 via the err and errstr attributes.
39 Reloading XSLT files can be done with the refresh method.
41 =head1 METHODS
43 =head2 new
45 Create handler object (via Class::Accessor)
47 =head2 transform
49 Run transformation for specific string and stylesheet
51 =head2 refresh
53 Allow to reload stylesheets when transforming again
55 =head1 PROPERTIES
57 =head2 err
59 Error number (see list of ERROR CODES)
61 =head2 errstr
63 Error message
65 =head2 do_not_return_source
67 If true, transform returns undef on failure. By default, it returns the
68 original string passed. Errors are reported as described.
70 =head2 print_warns
72 If set, print error messages to STDERR. True by default.
74 =head1 ERROR CODES
76 =head2 Error 1
78 No XSLT file passed
80 =head2 Error 2
82 XSLT file not found
84 =head2 Error 3
86 Error while loading stylesheet xml: [furter information]
88 =head2 Error 4
90 Error while parsing stylesheet: [furter information]
92 =head2 Error 5
94 Error while parsing input: [furter information]
96 =head2 Error 6
98 Error while transforming input: [furter information]
100 =head2 Error 7
102 No string to transform
104 =head1 INTERNALS
106 For documentation purposes. You are not encouraged to access them.
108 =head2 last_xsltfile
110 Contains the last successfully executed XSLT filename
112 =head2 xslt_hash
114 Hash reference to loaded stylesheets
116 =head1 ADDITIONAL COMMENTS
118 =cut
120 use Modern::Perl;
121 use XML::LibXML;
122 use XML::LibXSLT;
124 use base qw(Class::Accessor);
126 __PACKAGE__->mk_ro_accessors(qw( err errstr ));
127 __PACKAGE__->mk_accessors(qw( do_not_return_source print_warns ));
129 =head2 transform
131 my $output= $xslt_engine->transform( $xml, $xsltfilename );
132 if( $xslt_engine->err ) {
133 #decide what to do on failure..
135 my $output2= $xslt_engine->transform( $xml2 );
137 Pass a xml string and a fully qualified path of a XSLT file.
138 Instead of a filename, you may also pass a URL.
139 If you do not pass a filename, the last file used is assumed.
140 Returns the transformed string.
141 Check the error number in err to know if something went wrong.
142 In that case do_not_return_source did determine the return value.
144 =cut
146 sub transform {
147 my ( $self, $orgxml, $file ) = @_;
149 #Initialized yet?
150 if ( !$self->{xslt_hash} ) {
151 $self->_init;
153 else {
154 $self->_set_error; #clear error
156 my $retval = $self->{do_not_return_source} ? undef : $orgxml;
158 #check if no string passed
159 if ( !defined $orgxml ) {
160 $self->_set_error(7);
161 return; #always undef
164 #If no file passed, use the last file again
165 if ( !$file ) {
166 if ( !$self->{last_xsltfile} ) {
167 $self->_set_error(1);
168 return $retval;
170 $file = $self->{last_xsltfile};
173 #load stylesheet
174 my $stsh = $self->{xslt_hash}->{$file} // $self->_load($file);
175 return $retval if $self->{err};
177 #parse input and transform
178 my $parser = XML::LibXML->new();
179 my $source = eval { $parser->parse_string($orgxml) };
180 if ($@) {
181 $self->_set_error( 5, $@ );
182 return $retval;
184 my $str = eval {
185 my $result = $stsh->transform($source);
186 $stsh->output_as_chars($result);
188 if ($@) {
189 $self->_set_error( 6, $@ );
190 return $retval;
192 $self->{last_xsltfile} = $file;
193 return $str;
196 =head2 refresh
198 $xslt_engine->refresh;
199 $xslt_engine->refresh( $xsltfilename );
201 Pass a file for an individual refresh or no file to refresh all.
202 Refresh returns the number of items affected.
203 What we actually do, is just clear the internal cache for reloading next
204 time when transform is called.
205 The return value is mainly theoretical. Since this is supposed to work
206 always(...), there is no actual need to test it.
207 Note that refresh does also clear the error information.
209 =cut
211 sub refresh {
212 my ( $self, $file ) = @_;
213 $self->_set_error;
214 return if !$self->{xslt_hash};
215 my $rv;
216 if ($file) {
217 $rv = delete $self->{xslt_hash}->{$file} ? 1 : 0;
219 else {
220 $rv = scalar keys %{ $self->{xslt_hash} };
221 $self->{xslt_hash} = {};
223 return $rv;
226 # ************** INTERNAL ROUTINES ********************************************
228 # _init
229 # Internal routine for initialization.
231 sub _init {
232 my $self = shift;
234 $self->_set_error;
235 $self->{xslt_hash} = {};
236 $self->{print_warns} = 1 unless exists $self->{print_warns};
237 $self->{do_not_return_source} = 0
238 unless exists $self->{do_not_return_source};
240 #by default we return source on a failing transformation
241 #but it could be passed at construction time already
242 return;
245 # _load
246 # Internal routine for loading a new stylesheet.
248 sub _load {
249 my ( $self, $file ) = @_;
251 if ( !$file || ( $file !~ /^https?:\/\// && !-e $file ) ) {
252 $self->_set_error(2);
253 return;
256 #load sheet
257 my $parser = XML::LibXML->new;
258 my $style_doc = eval { $parser->load_xml( location => $file ) };
259 if ($@) {
260 $self->_set_error( 3, $@ );
261 return;
264 #parse sheet
265 my $xslt = XML::LibXSLT->new;
266 $self->{xslt_hash}->{$file} = eval { $xslt->parse_stylesheet($style_doc) };
267 if ($@) {
268 $self->_set_error( 4, $@ );
269 delete $self->{xslt_hash}->{$file};
270 return;
272 return $self->{xslt_hash}->{$file};
275 # _set_error
276 # Internal routine for handling error information.
278 sub _set_error {
279 my ( $self, $errno, $addmsg ) = @_;
281 if ( !$errno ) { #clear the error
282 $self->{err} = undef;
283 $self->{errstr} = undef;
284 return;
287 $self->{err} = $errno;
288 if ( $errno == 1 ) {
289 $self->{errstr} = "No XSLT file passed.";
291 elsif ( $errno == 2 ) {
292 $self->{errstr} = "XSLT file not found.";
294 elsif ( $errno == 3 ) {
295 $self->{errstr} = "Error while loading stylesheet xml:";
297 elsif ( $errno == 4 ) {
298 $self->{errstr} = "Error while parsing stylesheet:";
300 elsif ( $errno == 5 ) {
301 $self->{errstr} = "Error while parsing input:";
303 elsif ( $errno == 6 ) {
304 $self->{errstr} = "Error while transforming input:";
306 elsif ( $errno == 7 ) {
307 $self->{errstr} = "No string to transform.";
310 if ($addmsg) {
311 $self->{errstr} .= " $addmsg";
314 warn $self->{errstr} if $self->{print_warns};
315 return;
318 =head1 AUTHOR
320 Marcel de Rooy, Rijksmuseum Netherlands
322 =cut