File Coverage

blib/lib/Dancer/Plugin/Catmandu/SRU.pm
Criterion Covered Total %
statement 77 122 63.1
branch 7 28 25.0
condition 9 32 28.1
subroutine 11 11 100.0
pod n/a
total 104 193 53.8


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