Bug 11826: Add XSLT handler object to Koha namespace
[koha.git] / Koha / XSLT_Handler.pm
1 package Koha::XSLT_Handler;
2
3 # Copyright 2014 Rijksmuseum
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
15 #
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.
19
20 =head1 NAME
21
22 Koha::XSLT_Handler - Facilitate use of XSLT transformations
23
24 =head1 SYNOPSIS
25
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);
32
33 =head1 DESCRIPTION
34
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.
40
41 =head1 METHODS
42
43 =head2 new
44
45     Create handler object (via Class::Accessor)
46
47 =head2 transform
48
49     Run transformation for specific string and stylesheet
50
51 =head2 refresh
52
53     Allow to reload stylesheets when transforming again
54
55 =head1 PROPERTIES
56
57 =head2 err
58
59     Error number (see list of ERROR CODES)
60
61 =head2 errstr
62
63     Error message
64
65 =head2 do_not_return_source
66
67     If true, transform returns undef on failure. By default, it returns the
68     original string passed. Errors are reported as described.
69
70 =head1 ERROR CODES
71
72 =head2 Error 1
73
74     No XSLT file passed
75
76 =head2 Error 2
77
78     XSLT file not found
79
80 =head2 Error 3
81
82     Error while loading stylesheet xml: [furter information]
83
84 =head2 Error 4
85
86     Error while parsing stylesheet: [furter information]
87
88 =head2 Error 5
89
90     Error while parsing input: [furter information]
91
92 =head2 Error 6
93
94     Error while transforming input: [furter information]
95
96 =head2 Error 7
97
98     No string to transform
99
100 =head1 INTERNALS
101
102     For documentation purposes. You are not encouraged to access them.
103
104 =head2 last_xsltfile
105
106     Contains the last successfully executed XSLT filename
107
108 =head2 xslt_hash
109
110     Hash reference to loaded stylesheets
111
112 =head1 ADDITIONAL COMMENTS
113
114 =cut
115
116 use Modern::Perl;
117 use XML::LibXML;
118 use XML::LibXSLT;
119
120 use base qw(Class::Accessor);
121
122 __PACKAGE__->mk_ro_accessors(qw( err errstr ));
123 __PACKAGE__->mk_accessors(qw( do_not_return_source ));
124
125 =head2 transform
126
127     my $output= $xslt_engine->transform( $xml, $xsltfilename );
128     if( $xslt_engine->err ) {
129         #decide what to do on failure..
130     }
131     my $output2= $xslt_engine->transform( $xml2 );
132
133     Pass a xml string and a fully qualified path of a XSLT file.
134     If you do not pass a filename, the last file used is assumed.
135     Returns the transformed string.
136     Check the error number in err to know if something went wrong.
137     In that case do_not_return_source did determine the return value.
138
139 =cut
140
141 sub transform {
142     my ( $self, $orgxml, $file ) = @_;
143
144     #Initialized yet?
145     if( !$self->{xslt_hash} ) {
146         $self->_init;
147     }
148     else {
149         $self->_set_error; #clear error
150     }
151     my $retval= $self->{do_not_return_source}? undef: $orgxml;
152
153     #check if no string passed
154     if( !defined $orgxml ) {
155         $self->_set_error(7);
156         return; #always undef
157     }
158
159     #If no file passed, use the last file again
160     if( !$file ) {
161         if( !$self->{last_xsltfile} ) {
162             $self->_set_error(1);
163             return $retval;
164         }
165         $file= $self->{last_xsltfile};
166     }
167
168     #load stylesheet
169     my $stsh= $self->{xslt_hash}->{$file} // $self->_load($file);
170     return $retval if $self->{err};
171
172     #parse input and transform
173     my $parser = XML::LibXML->new();
174     my $source= eval { $parser->parse_string($orgxml) };
175     if( $@ ) {
176         $self->_set_error(5, $@);
177         return $retval;
178     }
179     my $str= eval {
180         my $result= $stsh->transform($source);
181         $stsh->output_as_chars($result);
182     };
183     if( $@ ) {
184         $self->_set_error(6, $@);
185         return $retval;
186     }
187     $self->{last_xsltfile}= $file;
188     return $str;
189 }
190
191 =head2 refresh
192
193     $xslt_engine->refresh;
194     $xslt_engine->refresh( $xsltfilename );
195
196     Pass a file for an individual refresh or no file to refresh all.
197     Refresh returns the number of items affected.
198     What we actually do, is just clear the internal cache for reloading next
199     time when transform is called.
200     The return value is mainly theoretical. Since this is supposed to work
201     always(...), there is no actual need to test it.
202     Note that refresh does also clear the error information.
203
204 =cut
205
206 sub refresh {
207     my ( $self, $file )= @_;
208     $self->_set_error;
209     return if !$self->{xslt_hash};
210     my $rv;
211     if( $file ) {
212         $rv= delete $self->{xslt_hash}->{$file}? 1: 0;
213     }
214     else {
215         $rv= scalar keys %{ $self->{xslt_hash} };
216         $self->{xslt_hash}= {};
217     }
218     return $rv;
219 }
220
221 # **************  INTERNAL ROUTINES ********************************************
222
223 # _init
224 # Internal routine for initialization.
225
226 sub _init {
227     my $self= shift;
228
229     $self->_set_error;
230     $self->{xslt_hash}={};
231     $self->{do_not_return_source}=0 unless exists $self->{do_not_return_source};
232         #by default we return source on a failing transformation
233         #but it could be passed at construction time already
234     return;
235 }
236
237 # _load
238 # Internal routine for loading a new stylesheet.
239
240 sub _load {
241     my ($self, $file)= @_;
242
243     if( !$file || !-e $file ) {
244         $self->_set_error(2);
245         return;
246     }
247
248     #load sheet
249     my $parser = XML::LibXML->new;
250     my $style_doc = eval { $parser->load_xml( location => $file ) };
251     if( $@ ) {
252         $self->_set_error(3, $@);
253         return;
254     }
255
256     #parse sheet
257     my $xslt = XML::LibXSLT->new;
258     $self->{xslt_hash}->{$file} = eval { $xslt->parse_stylesheet($style_doc) };
259     if( $@ ) {
260         $self->_set_error(4, $@);
261         delete $self->{xslt_hash}->{$file};
262         return;
263     }
264     return $self->{xslt_hash}->{$file};
265 }
266
267 # _set_error
268 # Internal routine for handling error information.
269
270 sub _set_error {
271     my ($self, $errno, $addmsg)= @_;
272
273     if(!$errno) { #clear the error
274         $self->{err}= undef;
275         $self->{errstr}= undef;
276         return;
277     }
278
279     $self->{err}= $errno;
280     if($errno==1) {
281         $self->{errstr}= "No XSLT file passed.";
282     }
283     elsif($errno==2) {
284         $self->{errstr}= "XSLT file not found.";
285     }
286     elsif($errno==3) {
287         $self->{errstr}= "Error while loading stylesheet xml:";
288     }
289     elsif($errno==4) {
290         $self->{errstr}= "Error while parsing stylesheet:";
291     }
292     elsif($errno==5) {
293         $self->{errstr}= "Error while parsing input:";
294     }
295     elsif($errno==6) {
296         $self->{errstr}= "Error while transforming input:";
297     }
298     elsif($errno==7) {
299         $self->{errstr}= "No string to transform.";
300     }
301
302     if( $addmsg ) {
303         $self->{errstr}.= " $addmsg";
304     }
305     return;
306 }
307
308 =head1 AUTHOR
309
310     Marcel de Rooy, Rijksmuseum Netherlands
311
312 =cut
313
314 1;