]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Fielder.pm
LP2061136 - Stamping 1405 DB upgrade script
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Fielder.pm
1 # vim:et:ts=4:sw=4:
2
3 package OpenILS::Application::Fielder;
4 use OpenILS::Application;
5 use base qw/OpenILS::Application/;
6
7 use Unicode::Normalize;
8 use OpenSRF::EX qw/:try/;
9
10 use OpenSRF::AppSession;
11 use OpenSRF::Utils::SettingsClient;
12 use OpenSRF::Utils::Cache;
13 use OpenSRF::Utils::Logger qw/:level/;
14
15 use OpenILS::Utils::Fieldmapper;
16 use OpenSRF::Utils::JSON;
17
18 use OpenILS::Utils::CStoreEditor qw/:funcs/;
19
20 use Digest::MD5 qw(md5_hex);
21
22 use XML::LibXML;
23 use XML::LibXML::XPathContext;
24 use XML::LibXSLT;
25
26 use OpenILS::Application::Flattener;
27 use Data::Dumper;
28
29 $Data::Dumper::Indent = 0;
30
31 our %namespace_map = (
32     oils_persist=> {ns => 'http://open-ils.org/spec/opensrf/IDL/persistence/v1'},
33     oils_obj    => {ns => 'http://open-ils.org/spec/opensrf/IDL/objects/v1'},
34     idl         => {ns => 'http://opensrf.org/spec/IDL/base/v1'},
35     reporter    => {ns => 'http://open-ils.org/spec/opensrf/IDL/reporter/v1'},
36     perm        => {ns => 'http://open-ils.org/spec/opensrf/IDL/permacrud/v1'},
37 );
38
39
40 my $log = 'OpenSRF::Utils::Logger';
41
42 my $cache;
43 my $cache_timeout;
44 my $default_locale;
45 my $parser = XML::LibXML->new();
46 my $xslt = XML::LibXSLT->new();
47
48 my $xpc = XML::LibXML::XPathContext->new();
49 $xpc->registerNs($_, $namespace_map{$_}{ns}) for ( keys %namespace_map );
50
51 my $idl;
52
53 sub initialize {
54
55     my $conf = OpenSRF::Utils::SettingsClient->new;
56     my $idl_file = $conf->config_value( 'IDL' );
57
58     $idl = $parser->parse_file( $idl_file );
59
60     $log->debug( 'IDL XML file loaded' );
61
62     $cache_timeout = $conf->config_value(
63             "apps", "open-ils.fielder", "app_settings", "cache_timeout" ) || 300;
64
65     $default_locale = $conf->config_value("default", "default_locale") || 'en-US';
66
67     generate_methods();
68
69 }
70 sub child_init {
71     $cache = OpenSRF::Utils::Cache->new('global');
72 }
73
74 sub fielder_fetch {
75     my $self = shift;
76     my $client = shift;
77     my $obj = shift;
78
79     my $locale = $self->session->session_locale || $default_locale;
80     my $query = $obj->{query};
81     my $nocache = $obj->{cache} ? 0 : 1;
82     my $fields = $obj->{fields};
83     my $distinct = $obj->{distinct} ? 1 : 0;
84
85     return OpenILS::Event->new('BAD_PARAMS', note => 'query is required') unless $query;
86     return OpenILS::Event->new('BAD_PARAMS', note => 'query must be a hash') unless ref($query) eq 'HASH';
87     return OpenILS::Event->new('BAD_PARAMS', note => 'query must not use function transforms')
88         if _fielder_fetch_query_is_bad($query, 0);
89     if ($fields) {
90         return OpenILS::Event->new('BAD_PARAMS', note => 'field list must not use function transforms')
91             if _fielder_fetch_query_is_bad($fields, 0);
92     }
93
94     my $obj_class = $self->{class_hint};
95     my $fm_class = $self->{class_name};
96
97     if (!$fields) {
98         $fields = [ $fm_class->real_fields ];
99     }
100
101     $fields = [$fields] if (!ref($fields));
102
103     my $qstring = OpenSRF::Utils::JSON->perl2JSON( $query );
104     my $fstring = OpenSRF::Utils::JSON->perl2JSON( [ sort { $a cmp $b } @$fields ] );
105
106     $log->debug( 'Query Class: '. $obj_class );
107     $log->debug( 'Field list: '. $fstring );
108     $log->debug( 'Query: '. $qstring );
109
110     my ($key,$res);
111     unless ($nocache) {
112         $key = 'open-ils.fielder_' . md5_hex(
113             $self->api_name . 
114             $qstring .
115             $fstring .
116             $distinct .
117             $obj_class .
118             $locale
119         );
120
121         $res = $cache->get_cache( $key );
122
123         if ($res) {
124             $client->respond($_) for (@$res);
125             return undef;
126         }
127     }
128
129     $res = new_editor()->json_query({
130         select  => { $obj_class => $fields },
131         from    => $obj_class,
132         where   => $query,
133         distinct=> $distinct
134     });
135
136     for my $value (@$res) {
137         $client->respond($value);
138     }
139
140     $client->respond_complete();
141
142     $cache->put_cache( $key => $res => $cache_timeout ) unless ($nocache);
143     return undef;
144 }
145
146 sub _fielder_fetch_query_is_bad {
147     my ($query, $depth) = @_;
148
149     if ($depth >= 10) {
150         # arbitrarily assume that something naughty is going
151         # on if the original structure is 10 layers deep
152         return 1;
153     }
154
155     if (ref $query eq 'HASH') {
156         return 1 if exists $query->{transform};
157         return 1 if exists $query->{params};
158         foreach my $key (keys %{ $query }) {
159             if (_fielder_fetch_query_is_bad($query->{$key}, $depth + 1)) {
160                 return 1;
161             }
162         }
163         return 0;
164     } elsif (ref $query eq 'ARRAY') {
165         foreach my $entry (@{ $query }) {
166             if (_fielder_fetch_query_is_bad($entry, $depth + 1)) {
167                 return 1;
168             }
169         }
170         return 0;
171     } else {
172         return 0;
173     }
174 }
175
176 sub generate_methods {
177     try {
178         for my $class_node ( $xpc->findnodes( '//idl:class[@oils_persist:field_safe="true"]', $idl->documentElement ) ) {
179             my $hint = $class_node->getAttribute('id');
180             my $fm = $class_node->getAttributeNS('http://open-ils.org/spec/opensrf/IDL/objects/v1','fieldmapper');
181             $log->debug("Fielder class_node $hint");
182         
183             __PACKAGE__->register_method(
184                 method          => 'fielder_fetch',
185                 api_name        => 'open-ils.fielder.' . $hint,
186                 class_hint      => $hint,
187                 class_name      => "Fieldmapper::$fm",
188                 stream          => 1,
189                 argc            => 1
190             );
191         }
192     } catch Error with {
193         my $e = shift;
194         $log->error("error generating Fielder methods: $e");
195     };
196 }
197
198 sub register_map {
199     my ($self, $conn, $auth, $hint, $map) = @_;
200
201     my $e = new_editor(authtoken => $auth);
202     return $e->event unless $e->checkauth;
203
204     $key = 'flat_search_' . md5_hex(
205         $hint .
206         OpenSRF::Utils::JSON->perl2JSON( $map )
207     );
208
209     $cache->put_cache( $key => { hint => $hint, map => $map } => $cache_timeout );
210 }
211
212 __PACKAGE__->register_method(
213     method          => 'register_map',
214     api_name        => 'open-ils.fielder.flattened_search.prepare',
215     argc            => 3,
216     signature       => {
217         params => [
218             {name => "auth", type => "string", desc => "auth token"},
219             {name => "hint", type => "string",
220                 desc => "fieldmapper class hint of core object"},
221             {name => "map", type => "object", desc => q{
222                 path-field mapping structure. See documentation under
223                 docs/TechRef/Flattener in the Evergreen source tree.} }
224         ],
225         return => {
226             desc => q{
227                 A key used to reference a prepared flattened search on subsequent
228                 calls to open-ils.fielder.flattened_search.execute},
229             type => "string"
230         }
231     }
232 );
233
234 sub execute_registered_flattened_search {
235     my $self = shift;
236     my $conn = shift;
237     my $auth = shift;
238     my $key  = shift;
239
240     my $e = new_editor(authtoken => $auth);
241     return $e->event unless $e->checkauth;
242     $e->disconnect;
243
244     my $blob = $cache->get_cache( $key ) or
245         return new OpenILS::Event('CACHE_MISS');
246
247     flattened_search( $self, $conn, $auth, $blob->{hint}, $blob->{map}, @_ )
248         if (ref($blob) and $blob->{hint} and $blob->{map});
249 }
250
251 __PACKAGE__->register_method(
252     method          => 'execute_registered_flattened_search',
253     api_name        => 'open-ils.fielder.flattened_search.execute',
254     stream          => 1,
255     argc            => 5,
256     signature       => {
257         params => [
258             {name => "auth", type => "string", desc => "auth token"},
259             {name => "key", type => "string",
260                 desc => "Key for a registered map provided by open-ils.fielder.flattened_search.prepare"},
261             {name => "where", type => "object", desc => q{
262                 simplified query clause (like the 'where' clause of a
263                 json_query, but different). See documentation under
264                 docs/TechRef/Flattener in the Evergreen source tree.} },
265             {name => "slo", type => "object", desc => q{
266                 simplified sort/limit/offset object. See documentation under
267                 docs/TechRef/Flattener in the Evergreen source tree.} }
268         ],
269         return => {
270             desc => q{
271                 A stream of objects flattened to your specifications. See
272                 documentation under docs/TechRef/Flattener in the Evergreen
273                 source tree.},
274             type => "object"
275         }
276     }
277 );
278
279 sub flattened_search {
280     my ($self, $conn, $auth, $hint, $map, $where, $slo) = @_;
281
282     # All but the last argument really are necessary.
283     $slo ||= {};
284
285     my $e = new_editor(authtoken => $auth);
286     return $e->event unless $e->checkauth;
287
288     # Process the map to normalize it, and to get all our joins and fleshing
289     # structure into the jffolo.
290     my $jffolo;
291     ($map, $jffolo) =
292         OpenILS::Application::Flattener::process_map($hint, $map);
293
294     # Process the suppied where clause, using our map, to make the
295     # filter.
296     my $filter = OpenILS::Application::Flattener::prepare_filter($map, $where);
297
298     # Process the supplied sort/limit/offset clause and use it to finish the
299     # jffolo.
300     $jffolo = OpenILS::Application::Flattener::finish_jffolo(
301         $hint, $map, $jffolo, $slo
302     );
303
304     # Reach out and touch pcrud (could be cstore, if we wanted to offer
305     # this as a private service).
306     my $pcrud = create OpenSRF::AppSession("open-ils.pcrud");
307     my $req = $pcrud->request(
308         "open-ils.pcrud.search.$hint", $auth, $filter, $jffolo
309     );
310
311     # Stream back flattened results.
312     while (my $resp = $req->recv(timeout => 60)) {
313         $conn->respond(
314             OpenILS::Application::Flattener::process_result(
315                 $map, $resp->content
316             )
317         );
318     }
319
320     # Clean up.
321     $pcrud->kill_me;
322
323     return;
324 }
325
326 __PACKAGE__->register_method(
327     method          => 'flattened_search',
328     api_name        => 'open-ils.fielder.flattened_search',
329     stream          => 1,
330     argc            => 5,
331     signature       => {
332         params => [
333             {name => "auth", type => "string", desc => "auth token"},
334             {name => "hint", type => "string",
335                 desc => "fieldmapper class hint of core object"},
336             {name => "map", type => "object", desc => q{
337                 path-field mapping structure. See documentation under
338                 docs/TechRef/Flattener in the Evergreen source tree.} },
339             {name => "where", type => "object", desc => q{
340                 simplified query clause (like the 'where' clause of a
341                 json_query, but different). See documentation under
342                 docs/TechRef/Flattener in the Evergreen source tree.} },
343             {name => "slo", type => "object", desc => q{
344                 simplified sort/limit/offset object. See documentation under
345                 docs/TechRef/Flattener in the Evergreen source tree.} }
346         ],
347         return => {
348             desc => q{
349                 A stream of objects flattened to your specifications. See
350                 documentation under docs/TechRef/Flattener in the Evergreen
351                 source tree.},
352             type => "object"
353         }
354     }
355 );
356
357 1;
358