File Coverage

blib/lib/Dancer/Plugin/Catmandu/SRU.pm
Criterion Covered Total %
statement 74 119 62.1
branch 7 28 25.0
condition 9 32 28.1
subroutine 10 10 100.0
pod n/a
total 100 189 52.9


line stmt bran cond sub pod time code
1             package Dancer::Plugin::Catmandu::SRU;
2              
3             =head1 NAME
4              
5             Dancer::Plugin::Catmandu::SRU - SRU server backed by a searchable Catmandu::Store
6              
7             =cut
8              
9             our $VERSION = '0.0503';
10              
11 2     2   313694 use Catmandu::Sane;
  2         325566  
  2         15  
12 2     2   1544 use Catmandu;
  2         208333  
  2         9  
13 2     2   436 use Catmandu::Fix;
  2         4  
  2         44  
14 2     2   860 use Catmandu::Exporter::Template;
  2         93187  
  2         80  
15 2     2   1054 use SRU::Request;
  2         64852  
  2         72  
16 2     2   924 use SRU::Response;
  2         14607  
  2         61  
17 2     2   706 use Dancer qw(:syntax);
  2         174001  
  2         14  
18 2     2   1633 use Dancer::Plugin;
  2         2736  
  2         2957  
19              
20             sub sru_provider {
21 1     1   28022 my ($path) = @_;
22              
23 1         6 my $setting = plugin_setting;
24              
25 1   50     25 my $content_type = $setting->{content_type} // 'text/xml';
26              
27 1         5 my $default_record_schema = $setting->{default_record_schema};
28              
29 1         3 my $record_schemas = $setting->{record_schemas};
30              
31 1         3 my $record_schema_map = {};
32 1         4 for my $schema (@$record_schemas) {
33 1         5 $schema = {%$schema};
34 1         5 my $identifier = $schema->{identifier};
35 1         2 my $name = $schema->{name};
36 1 50       16 if (my $fix = $schema->{fix}) {
37 1         16 $schema->{fix} = Catmandu::Fix->new(fixes => $fix);
38             }
39 1         1996 $record_schema_map->{$identifier} = $schema;
40 1         4 $record_schema_map->{$name} = $schema;
41             }
42              
43 1         9 my $bag = Catmandu->store($setting->{store})->bag($setting->{bag});
44              
45 1   33     117645 my $default_limit = $setting->{limit} // $bag->default_limit;
46 1   33     6 my $maximum_limit = $setting->{maximum_limit} // $bag->maximum_limit;
47              
48 1   50     7 my $template_options = $setting->{template_options} || {};
49              
50 1         4 my $database_info = "";
51 1 50 33     9 if ($setting->{title} || $setting->{description}) {
52 0         0 $database_info .= qq(<databaseInfo>\n);
53 0         0 for my $key (qw(title description)) {
54 0 0       0 $database_info .= qq(<$key lang="en" primary="true">$setting->{$key}</$key>\n) if $setting->{$key};
55             }
56 0         0 $database_info .= qq(</databaseInfo>);
57             }
58              
59 1         3 my $index_info = "";
60 1 50 33     9 if ($bag->can('cql_mapping') and my $indexes = $bag->cql_mapping->{indexes}) { # TODO all Searchable should have cql_mapping
61 0         0 $index_info .= qq(<indexInfo>\n);
62 0         0 for my $key (keys %$indexes) {
63 0   0     0 my $title = $indexes->{$key}{title} || $key;
64 0         0 $index_info .= qq(<index><title>$title</title><map><name>$key</name></map></index>\n);
65             }
66 0         0 $index_info .= qq(</indexInfo>);
67             }
68              
69 1         3 my $schema_info = qq(<schemaInfo>\n);
70 1         4 for my $schema (@$record_schemas) {
71 1   33     10 my $title = $schema->{title} || $schema->{name};
72 1         7 $schema_info .= qq(<schema name="$schema->{name}" identifier="$schema->{identifier}"><title>$title</title></schema>\n);
73             }
74 1         14 $schema_info .= qq(</schemaInfo>);
75              
76 1         2 my $config_info = qq(<configInfo>\n);
77 1         4 $config_info .= qq(<default type="numberOfRecords">$default_limit</default>\n);
78 1         3 $config_info .= qq(<setting type="maximumRecords">$maximum_limit</setting>\n);
79 1         2 $config_info .= qq(</configInfo>);
80              
81             get $path => sub {
82 4     4   10774 content_type $content_type;
83              
84 4         374 my $params = params('query');
85 4   100     689 my $operation = $params->{operation} // 'explain';
86              
87 4 100       14 if ($operation eq 'explain') {
    50          
88 3         18 my $request = SRU::Request::Explain->new(%$params);
89 3         50 my $response = SRU::Response->newFromRequest($request);
90              
91 3         384 my $transport = request->scheme;
92 3         83 my $database = substr request->path, 1;
93 3         33 my $uri = request->uri_for(request->path_info);
94 3         8717 my $host = $uri->host;
95 3         74 my $port = $uri->port;
96 3         90 $response->record(SRU::Response::Record->new(
97             recordSchema => 'http://explain.z3950.org/dtd/2.1/',
98             recordData => <<XML,
99             <explain xmlns="http://explain.z3950.org/dtd/2.1/">
100             <serverInfo protocol="SRU" method="GET" transport="$transport">
101             <host>$host</host>
102             <port>$port</port>
103             <database>$database</database>
104             </serverInfo>
105             $database_info
106             $index_info
107             $schema_info
108             $config_info
109             </explain>
110             XML
111             ));
112 3         109 return $response->asXML;
113             }
114             elsif ($operation eq 'searchRetrieve') {
115 1         9 my $request = SRU::Request::SearchRetrieve->new(%$params);
116 1         21 my $response = SRU::Response->newFromRequest($request);
117 1 50       312 if (@{$response->diagnostics}) {
  1         4  
118 1         14 return $response->asXML;
119             }
120              
121 0   0       my $schema = $record_schema_map->{$request->recordSchema || $default_record_schema};
122 0 0         unless ($schema) {
123 0           $response->addDiagnostic(SRU::Response::Diagnostic->newFromCode(66));
124 0           return $response->asXML;
125             }
126 0           my $identifier = $schema->{identifier};
127 0           my $fix = $schema->{fix};
128 0           my $template = $schema->{template};
129 0           my $layout = $schema->{layout};
130 0           my $cql = $params->{query};
131 0 0         if ($setting->{cql_filter}) {
132             # space before the filter is to circumvent a bug in the Solr
133             # 3.6 edismax parser
134 0           $cql = "( $setting->{cql_filter}) and ( $cql)";
135             }
136              
137 0   0       my $first = $request->startRecord // 1;
138 0   0       my $limit = $request->maximumRecords // $default_limit;
139 0 0         if ($limit > $maximum_limit) {
140 0           $limit = $maximum_limit;
141             }
142              
143             my $hits = eval {
144             $bag->search(
145 0 0         %{ $setting->{default_search_params} || {} },
  0            
146             cql_query => $cql,
147             sru_sortkeys => $request->sortKeys,
148             limit => $limit,
149             start => $first - 1,
150             );
151 0 0         } or do {
152 0           my $e = $@;
153 0 0         if ($e =~ /^cql error/) {
154 0           $response->addDiagnostic(SRU::Response::Diagnostic->newFromCode(10));
155 0           return $response->asXML;
156             }
157 0           die $e;
158             };
159              
160             $hits->each(sub {
161 0           my $data = $_[0];
162 0           my $metadata = "";
163 0           my $exporter = Catmandu::Exporter::Template->new(
164             %$template_options,
165             template => $template,
166             file => \$metadata
167             );
168 0 0         $exporter->add($fix ? $fix->fix($data) : $data);
169 0           $exporter->commit;
170 0           $response->addRecord(SRU::Response::Record->new(
171             recordSchema => $identifier,
172             recordData => $metadata,
173             ));
174 0           });
175 0           $response->numberOfRecords($hits->total);
176 0           return $response->asXML;
177             }
178             else {
179 0           my $request = SRU::Request::Explain->new(%$params);
180 0           my $response = SRU::Response->newFromRequest($request);
181 0           $response->addDiagnostic(SRU::Response::Diagnostic->newFromCode(6));
182 0           return $response->asXML;
183             }
184 1         25 };
185             }
186              
187             register sru_provider => \&sru_provider;
188              
189             register_plugin;
190              
191             1;
192              
193             =head1 SYNOPSIS
194              
195             #!/usr/bin/env perl
196            
197             use Dancer;
198             use Catmandu;
199             use Dancer::Plugin::Catmandu::SRU;
200            
201             Catmandu->load;
202             Catmandu->config;
203            
204             my $options = {};
205              
206             sru_provider '/sru', %$options;
207            
208             dance;
209              
210             =head1 DESCRIPTION
211              
212             L<Dancer::Plugin::Catmandu::SRU> is a Dancer plugin to provide SRU services for L<Catmandu::Store>-s that support
213             CQL (such as L<Catmandu::Store::ElasticSearch>). Follow the installation steps below to setup your own SRU server.
214              
215             =head1 REQUIREMENTS
216              
217             In the examples below an ElasticSearch 1.7.2 L<https://www.elastic.co/downloads/past-releases/elasticsearch-1-7-2> server
218             will be used:
219              
220             $ cpanm Dancer Catmandu::SRU Catmandu::Store::ElasticSearch
221              
222             $ wget https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.7.2.zip
223             $ unzip elasticsearch-1.7.2.zip
224             $ cd elasticsearch-1.7.2
225             $ bin/elasticsearch
226              
227             =head1 RECORDS
228              
229             Records stored in the Catmandu::Store can be in any format. Preferably the format should be easy to convert into an
230             XML format. At a minimum each record contains an identifier '_id'. In the examples below we'll configure the SRU
231             to serve Dublin Core records:
232              
233             $ cat sample.yml
234             ---
235             _id: 1
236             creator:
237             - Musterman, Max
238             - Jansen, Jan
239             - Svenson, Sven
240             title:
241             - Test record
242             ...
243              
244             =head1 CATMANDU CONFIGURATION
245              
246             ElasticSearch requires a configuration file to map record fields to CQL terms. Below is a minimal configuration
247             required to query for '_id' and 'title' and 'creator' in the ElasticSearch collection:
248              
249             $ cat catmandu.yml
250             ---
251             store:
252             sru:
253             package: ElasticSearch
254             options:
255             index_name: sru
256             bags:
257             data:
258             cql_mapping:
259             default_index: basic
260             indexes:
261             _id:
262             op:
263             'any': true
264             'all': true
265             '=': true
266             'exact': true
267             field: '_id'
268             creator:
269             op:
270             'any': true
271             'all': true
272             '=': true
273             'exact': true
274             field: 'creator'
275             title:
276             op:
277             'any': true
278             'all': true
279             '=': true
280             'exact': true
281             field: 'title'
282              
283             =head1 IMPORT RECORDS
284              
285             With the Catmandu configuration files in place records can be imported with the L<catmandu> command:
286              
287             # Drop the existing ElasticSearch 'sru' collection
288             $ catmandu drop sru
289              
290             # Import the sample record
291             $ catmandu import YAML to sru < sample.yml
292              
293             # Test if the records are available in the 'sru' collection
294             $ catmandu export sru
295              
296             =head1 DANCER CONFIGURATION
297              
298             The Dancer configuration file 'config.yml' contains basic information for the Catmandu::SRU plugin to work:
299              
300             * store - In which Catmandu::Store are the metadata records stored
301             * bag - In which Catmandu::Bag are the records of this 'store' (use: 'data' as default)
302             * cql_filter - A CQL query to find all records in the database that should be made available to SRU
303             * default_record_schema - The metadataSchema to present records in
304             * limit - The maximum number of records to be returned in each SRU request
305             * maximum_limit - The maximum number of search results to return
306             * record_schemas - An array of all supported record schemas
307             * identifier - The SRU identifier for the schema (see L<http://www.loc.gov/standards/sru/recordSchemas/>)
308             * name - A short descriptive name for the schema
309             * fix - Optionally an array of fixes to apply to the records before they are transformed into XML
310             * template - The path to a Template Toolkit file to transform your records into this format
311             * template_options - An optional hash of configuration options that will be passed to L<Catmandu::Exporter::Template> or L<Template>
312             * content_type - Set a custom content type header, the default is 'text/xml'.
313              
314             Below is a sample minimal configuration for the 'sample.yml' demo above:
315              
316             charset: "UTF-8"
317             plugins:
318             'Catmandu::SRU':
319             store: sru
320             bag: data
321             default_record_schema: dc
322             limit: 200
323             maximum_limit: 500
324             record_schemas:
325             -
326             identifier: "info:srw/schema/1/dc-v1.1"
327             name: dc
328             template: dc.tt
329              
330             =head1 METADATA FORMAT TEMPLATE
331              
332             For each metadata format a Template Toolkit file needs to exist which translate L<Catmandu::Store> records
333             into XML records. The example below contains an example file to transform 'sample.yml' type records into
334             SRU DC:
335              
336             $ cat dc.tt
337             <srw_dc:dc xmlns:srw_dc="info:srw/schema/1/dc-schema"
338             xmlns:dc="http://purl.org/dc/elements/1.1/"
339             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
340             xsi:schemaLocation="info:srw/schema/1/dc-schema http://www.loc.gov/standards/sru/recordSchemas/dc-schema.xsd">
341             [%- FOREACH var IN ['title' 'creator' 'subject' 'description' 'publisher' 'contributor' 'date' 'type' 'format' 'identifier' 'source' 'language' 'relation' 'coverage' 'rights'] %]
342             [%- FOREACH val IN $var %]
343             <dc:[% var %]>[% val | html %]</dc:[% var %]>
344             [%- END %]
345             [%- END %]
346             </srw_dc:dc>
347              
348             =head1 START DANCER
349              
350             If all the required files are available, then a Dancer application can be started. See the 'demo' directory of
351             this distribution for a complete example:
352              
353             $ ls
354             app.pl catmandu.yml config.yml dc.tt
355             $ cat app.pl
356             #!/usr/bin/env perl
357            
358             use Dancer;
359             use Catmandu;
360             use Dancer::Plugin::Catmandu::SRU;
361            
362             Catmandu->load;
363             Catmandu->config;
364            
365             my $options = {};
366              
367             sru_provider '/sru', %$options;
368            
369             dance;
370              
371             # Start Dancer
372             $ perl ./app.pl
373            
374             # Test queries:
375             $ curl "http://localhost:3000/sru"
376             $ curl "http://localhost:3000/sru?version=1.1&operation=searchRetrieve&query=(_id+%3d+1)"
377             $ catmandu convert SRU --base 'http://localhost:3000/sru' --query '(_id = 1)'
378              
379              
380             =head1 AUTHOR
381              
382             Nicolas Steenlant, C<< <nicolas.steenlant at ugent.be> >>
383              
384             =head1 CONTRIBUTOR
385              
386             Vitali Peil, C<< <vitali.peil at uni-bielefeld.de> >>
387              
388             Patrick Hochstenbach, C<< <patrick.hochstenbach at ugent.be> >>
389              
390             =head1 SEE ALSO
391              
392             L<SRU>, L<Catmandu>, L<Catmandu::Store::ElasticSearch> , L<Catmandu::SRU>
393              
394             =head1 LICENSE
395              
396             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
397              
398             =cut