File Coverage

blib/lib/LabKey/Query.pm
Criterion Covered Total %
statement 42 216 19.4
branch 0 72 0.0
condition 0 40 0.0
subroutine 14 27 51.8
pod 5 5 100.0
total 61 360 16.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             LabKey::Query
6              
7             =head1 SYNOPSIS
8              
9             use LabKey::Query;
10             my $results = LabKey::Query::selectRows(
11             -baseUrl => 'http://labkey.com:8080/labkey/',
12             -containerPath => 'myFolder/',
13             -schemaName => 'lists',
14             -queryName => 'mid_tags',
15             );
16            
17             =head1 ABSTRACT
18              
19             For interacting with data in LabKey Server
20              
21             =head1 DESCRIPTION
22              
23             This module is designed to simplify querying and manipulating data in LabKey Server. It should more or less replicate the javascript APIs of the same names.
24              
25             After the module is installed, if you need to login with a specific user you
26             will need to create a L<.netrc|https://www.labkey.org/Documentation/wiki-page.view?name=netrc>
27             file in the home directory of the user running the perl script.
28              
29             In API versions 0.08 and later, you can specify the param '-loginAsGuest'
30             which will query the server without any credentials. The server must permit
31             guest to that folder for this to work though.
32              
33             =cut
34              
35             package LabKey::Query;
36              
37 6     6   371300 use warnings;
  6         51  
  6         187  
38 6     6   27 use strict;
  6         9  
  6         104  
39 6     6   3191 use JSON;
  6         65529  
  6         32  
40 6     6   3915 use Data::Dumper;
  6         33305  
  6         403  
41 6     6   2283 use FileHandle;
  6         52192  
  6         34  
42 6     6   1708 use File::Spec;
  6         12  
  6         141  
43 6     6   2821 use File::HomeDir;
  6         28541  
  6         272  
44 6     6   40 use Carp;
  6         11  
  6         254  
45              
46              
47             # Force all SSL connections to use TLSv1 or greater protocol. This is required for
48             # MacOSX and older Windows workstations.
49             #
50             # Credit to @chrisrth on stackoverflow (http://stackoverflow.com/a/20305596)
51             # See https://www.labkey.org/issues/home/Developer/issues/details.view?issueId=22146
52             # for more information.
53             #
54 6     6   3968 use IO::Socket::SSL;
  6         402592  
  6         55  
55             my $context = new IO::Socket::SSL::SSL_Context(
56             SSL_version => 'tlsv1'
57             );
58             IO::Socket::SSL::set_default_context($context);
59              
60 6     6   4976 use LWP::UserAgent;
  6         253608  
  6         224  
61 6     6   3266 use HTTP::Cookies;
  6         35883  
  6         170  
62 6     6   2507 use HTTP::Request::Common;
  6         11176  
  6         387  
63 6     6   43 use URI;
  6         9  
  6         152  
64              
65              
66 6     6   28 use vars qw($VERSION);
  6         12  
  6         18629  
67              
68             our $VERSION = "1.06";
69              
70              
71              
72             =head1 FUNCTIONS
73              
74             =head2 selectRows()
75              
76             selectRows() can be used to query data from LabKey server
77              
78             The following are the minimum required params:
79            
80             my $results = LabKey::Query::selectRows(
81             -baseUrl => 'http://labkey.com:8080/labkey/',
82             -containerPath => 'myFolder/',
83             -schemaName => 'lists',
84             -queryName => 'mid_tags',
85             );
86              
87             The following are optional:
88              
89             -viewName => 'view1',
90             -filterArray => [
91             ['file_active', 'eq', 1],
92             ['species', 'neq', 'zebra']
93             ], #allows filters to be applied to the query similar to the labkey Javascript API.
94             -parameters => [
95             ['enddate', '2011/01/01'],
96             ['totalDays', 15]
97             ], #allows parameters to be applied to the query similar to the labkey Javascript API.
98             -maxRows => 10 #the max number of rows returned
99             -sort => 'ColumnA,ColumnB' #sort order used for this query
100             -offset => 100 #the offset used when running the query
101             -columns => 'ColumnA,ColumnB' #A comma-delimited list of column names to include in the results.
102             -containerFilterName => 'currentAndSubfolders'
103             -debug => 1, #will result in a more verbose output
104             -loginAsGuest => #will not attempt to lookup credentials in netrc
105             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
106             -requiredVersion => 9.1 #if 8.3 is selected, it will use LabKey's pre-9.1 format for returning the data. 9.1 is the default. See documentation of LABKEY.Query.ExtendedSelectRowsResults for more detail here:
107             https://www.labkey.org/download/clientapi_docs/javascript-api/symbols/LABKEY.Query.html
108             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
109             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
110            
111             =head3 NOTE
112              
113             In version 1.0 and later of the perl API, the default result format is 9.1. This is different from the LabKey JS, which defaults to the earlier format for legacy purposes.
114              
115             =cut
116              
117             sub selectRows {
118 0     0 1   my %args = @_;
119              
120 0           my @required = ('-queryName', '-schemaName');
121 0           _checkRequiredParams(\%args, \@required);
122              
123 0           my $ctx = _getServerContext(%args);
124              
125             my $data = {
126             'schemaName' => $args{'-schemaName'},
127             'query.queryName' => $args{'-queryName'},
128 0   0       'apiVersion' => $args{'-requiredVersion'} || 9.1
129             };
130              
131 0           foreach (@{$args{-filterArray}}) {
  0            
132 0           $$data{'query.' . @{$_}[0] . '~' . @{$_}[1]} = @{$_}[2];
  0            
  0            
  0            
133             }
134              
135 0           foreach (@{$args{-parameters}}) {
  0            
136 0           $$data{'query.param.' . @{$_}[0]} = @{$_}[1];
  0            
  0            
137             }
138            
139 0           foreach ('viewName', 'offset', 'sort', 'maxRows', 'columns', 'containerFilterName') {
140 0 0         if ($args{'-' . $_}) {
141 0           $$data{'query.' . $_} = $args{'-' . $_};
142             }
143             }
144            
145 0           return _postData($ctx, _buildURL($ctx, 'query', 'getQuery.api'), $data);
146             }
147              
148              
149             =head2 insertRows()
150              
151             insertRows() can be used to insert records into a LabKey table
152              
153             The following are the minimum required params:
154              
155             my $insert = LabKey::Query::insertRows(
156             -baseUrl => 'http://labkey.com:8080/labkey/',
157             -containerPath => 'myFolder/',
158             -schemaName => 'lists',
159             -queryName => 'backup',
160             -rows => [{
161             "JobName" => 'jobName',
162             "Status" => $status,
163             "Log" => $log,
164             "Date" => $date
165             }],
166             );
167            
168             The following are optional:
169              
170             -debug => 1, #will result in a more verbose output
171             -loginAsGuest => #will not attempt to lookup credentials in netrc
172             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
173             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
174             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
175              
176             =cut
177              
178             sub insertRows {
179 0     0 1   my %args = @_;
180              
181 0           my @required = ('-queryName', '-schemaName', '-rows');
182 0           _checkRequiredParams(\%args, \@required);
183              
184 0           my $ctx = _getServerContext(%args);
185              
186             my $data = {
187             "schemaName" => $args{'-schemaName'},
188             "queryName" => $args{'-queryName'},
189 0           "rows" => $args{'-rows'}
190             };
191              
192 0           return _postData($ctx, _buildURL($ctx, 'query', 'insertRows.api'), $data);
193             }
194              
195              
196             =head2 updateRows()
197              
198             updateRows() can be used to update records in a LabKey table
199              
200             The following are the minimum required params:
201              
202             my $update = LabKey::Query::updateRows(
203             -baseUrl => 'http://labkey.com:8080/labkey/',
204             -containerPath => 'myFolder/',
205             -schemaName => 'lists',
206             -queryName => 'backup',
207             -rows => [{
208             "JobName" => 'jobName',
209             "Status" => $status,
210             "Log" => $log,
211             "Date" => $date
212             }],
213             );
214            
215             The following are optional:
216              
217             -debug => 1, #will result in a more verbose output
218             -loginAsGuest => #will not attempt to lookup credentials in netrc
219             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
220             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
221             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
222              
223             =cut
224              
225             sub updateRows {
226 0     0 1   my %args = @_;
227              
228 0           my @required = ('-queryName', '-schemaName', '-rows');
229 0           _checkRequiredParams(\%args, \@required);
230              
231 0           my $ctx = _getServerContext(%args);
232              
233             my $data = {
234             "schemaName" => $args{'-schemaName'},
235             "queryName" => $args{'-queryName'},
236 0           "rows" => $args{'-rows'}
237             };
238              
239 0           return _postData($ctx, _buildURL($ctx, 'query', 'updateRows.api'), $data);
240             }
241              
242              
243             =head2 deleteRows()
244              
245             deleteRows() can be used to delete records in a LabKey table
246              
247             The following are the minimum required params:
248              
249             my $update = LabKey::Query::deleteRows(
250             -baseUrl => 'http://labkey.com:8080/labkey/',
251             -containerPath => 'myFolder/',
252             -schemaName => 'lists',
253             -queryName => 'backup',
254             -rows => [{
255             "Key" => '12',
256             }],
257             );
258            
259             The following are optional:
260              
261             -debug => 1, #will result in a more verbose output
262             -loginAsGuest => #will not attempt to lookup credentials in netrc
263             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
264             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
265             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
266              
267             =cut
268              
269             sub deleteRows {
270 0     0 1   my %args = @_;
271              
272 0           my @required = ('-queryName', '-schemaName', '-rows');
273 0           _checkRequiredParams(\%args, \@required);
274              
275 0           my $ctx = _getServerContext(%args);
276              
277             my $data = {
278             "schemaName" => $args{'-schemaName'},
279             "queryName" => $args{'-queryName'},
280 0           "rows" => $args{'-rows'}
281             };
282              
283 0           return _postData($ctx, _buildURL($ctx, 'query', 'deleteRows.api'), $data);
284             }
285              
286              
287             =head2 executeSql()
288              
289             executeSql() can be used to execute arbitrary SQL
290              
291             The following are the minimum required params:
292              
293             my $result = LabKey::Query::executeSql(
294             -baseUrl => 'http://labkey.com:8080/labkey/',
295             -containerPath => 'myFolder/',
296             -schemaName => 'study',
297             -sql => 'select MyDataset.foo, MyDataset.bar from MyDataset',
298             );
299            
300             The following are optional:
301              
302             -maxRows => 10 #the max number of rows returned
303             -sort => 'ColumnA,ColumnB' #sort order used for this query
304             -offset => 100 #the offset used when running the query
305             -containerFilterName => 'currentAndSubfolders'
306             -debug => 1, #will result in a more verbose output
307             -loginAsGuest => #will not attempt to lookup credentials in netrc
308             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
309             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
310             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
311              
312             =cut
313              
314             sub executeSql {
315 0     0 1   my %args = @_;
316              
317 0           my @required = ('-schemaName', '-sql');
318 0           _checkRequiredParams(\%args, \@required);
319              
320 0           my $ctx = _getServerContext(%args);
321              
322             my $data = {
323             "schemaName" => $args{'-schemaName'},
324 0           "sql" => $args{'-sql'}
325             };
326            
327 0           foreach ('offset', 'sort', 'maxRows', 'containerFilterName') {
328 0 0         if ($args{'-' . $_}) {
329 0           $$data{$_} = $args{'-' . $_};
330             }
331             }
332            
333 0           return _postData($ctx, _buildURL($ctx, 'query', 'executeSql.api'), $data);
334             }
335              
336              
337             # NOTE: this code adapted from Net::Netrc module. It was changed so alternate locations could be supplied for a .netrc file
338             sub _readrc {
339              
340 0   0 0     my $host = shift || 'default';
341 0           my $file = shift;
342              
343             #allow user to supply netrc location
344 0 0 0       if(!$file || !-e $file){
345 0           $file = File::Spec->catfile( File::HomeDir::home(), '.netrc' );
346             }
347 0 0         if ( !-e $file ) {
348 0           $file = File::Spec->catfile( File::HomeDir::home(), '_netrc' );
349             }
350              
351 0           my %netrc = ();
352 0           my $fh;
353 0           local $_;
354              
355 0           $netrc{default} = undef;
356              
357             # OS/2 and Win32 do not handle stat in a way compatable with this check :-(
358 0 0 0       unless ( $^O eq 'os2'
      0        
      0        
      0        
359             || $^O eq 'MSWin32'
360             || $^O eq 'MacOS'
361             || $^O eq 'darwin'
362             || $^O =~ /^cygwin/ )
363             {
364 0           my @stat = stat($file);
365              
366 0 0         if (@stat) {
367 0 0         if ( $stat[2] & 077 ) {
368 0           carp "Bad permissions: $file";
369 0           return;
370             }
371 0 0         if ( $stat[4] != $< ) {
372 0           carp "Not owner: $file";
373 0           return;
374             }
375             }
376             }
377              
378 0 0         if ( $fh = FileHandle->new( $file, "r" ) ) {
379 0           my ( $mach, $macdef, $tok, @tok ) = ( 0, 0 );
380              
381 0           while (<$fh>) {
382 0 0         undef $macdef if /\A\n\Z/;
383              
384 0 0         if ($macdef) {
385 0           push( @$macdef, $_ );
386 0           next;
387             }
388              
389 0           s/^\s*//;
390 0           chomp;
391              
392 0   0       while ( length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*// )
393             {
394 0           ( my $tok = $+ ) =~ s/\\(.)/$1/g;
395 0           push( @tok, $tok );
396             }
397              
398             TOKEN:
399 0           while (@tok) {
400 0 0         if ( $tok[0] eq "default" ) {
401 0           shift(@tok);
402 0           $mach = bless {};
403 0           $netrc{default} = [$mach];
404              
405 0           next TOKEN;
406             }
407              
408             last TOKEN
409 0 0         unless @tok > 1;
410              
411 0           $tok = shift(@tok);
412              
413 0 0         if ( $tok eq "machine" ) {
    0          
    0          
414 0           my $host = shift @tok;
415 0           $mach = { machine => $host };
416              
417             $netrc{$host} = []
418 0 0         unless exists( $netrc{$host} );
419 0           push( @{ $netrc{$host} }, $mach );
  0            
420             }
421             elsif ( $tok =~ /^(login|password|account)$/ ) {
422 0 0         next TOKEN unless $mach;
423 0           my $value = shift @tok;
424              
425             # Following line added by rmerrell to remove '/' escape char in .netrc
426 0           $value =~ s/\/\\/\\/g;
427 0           $mach->{$1} = $value;
428             }
429             elsif ( $tok eq "macdef" ) {
430 0 0         next TOKEN unless $mach;
431 0           my $value = shift @tok;
432             $mach->{macdef} = {}
433 0 0         unless exists $mach->{macdef};
434 0           $macdef = $mach->{machdef}{$value} = [];
435             }
436             }
437             }
438 0           $fh->close();
439             }
440            
441 0           my $auth = $netrc{$host}[0];
442              
443             #if no machine is specified and there is only 1 machine in netrc, we use that one
444 0 0 0       if (!$auth && length((keys %netrc))==1){
445 0           $auth = $netrc{(keys %netrc)[0]}[0];
446             }
447              
448 0 0         warn("Unable to find entry for host: $host") unless $auth;
449 0 0         warn("Missing password for host: $host") unless $auth->{password};
450 0 0         warn("Missing login for host: $host") unless $auth->{login};
451              
452 0           return $auth;
453             }
454              
455              
456             sub _normalizeSlash {
457 0     0     my ($containerPath) = @_;
458            
459 0           $containerPath =~ s/^\///;
460 0           $containerPath =~ s/\/$//;
461 0           $containerPath .= '/';
462 0           return $containerPath;
463             }
464              
465              
466             sub _postData {
467 0     0     my ($ctx, $url, $data) = @_;
468              
469 0 0         print "POST " . $url . "\n" if $$ctx{debug};
470 0 0         print Dumper($data) if $$ctx{debug};
471              
472 0           my $json_obj = JSON->new->utf8->encode($data);
473              
474 0           my $req = POST $url;
475 0           $req->content_type('application/json');
476 0           $req->content($json_obj);
477 0           $req->authorization_basic($$ctx{auth}{'login'}, $$ctx{auth}{'password'});
478              
479 0           my $response = $$ctx{userAgent}->request($req);
480              
481             # Simple error checking
482 0 0         if ( $response->is_error ) {
483 0           croak($response->status_line . '\n' . $response->decoded_content);
484             }
485              
486 0   0       my $response_json = JSON->new->utf8->decode( $response->content )
487             || croak("ERROR: Unable to decode JSON.\n$url\n");
488            
489 0           return $response_json;
490             }
491              
492             sub _createUserAgent {
493 0     0     my %args = @_;
494              
495 0           my $ua = LWP::UserAgent->new;
496 0           $ua->agent("Perl API Client/1.0");
497 0           $ua->cookie_jar(HTTP::Cookies->new());
498              
499 0 0         if ($args{'-timeout'}) {
500 0           print "setting timeout to " . $args{'-timeout'} . "\n";
501 0           $ua->timeout($args{'-timeout'});
502             }
503 0           return $ua;
504             }
505              
506             sub _buildURL {
507 0     0     my ($ctx, $controller, $action) = @_;
508              
509             return URI->new(
510             _normalizeSlash($$ctx{baseUrl})
511             . _normalizeSlash($controller)
512             . _normalizeSlash($$ctx{containerPath})
513 0           . $action
514             . '?'
515             );
516             }
517              
518             sub _checkRequiredParams {
519 0     0     my %args = %{$_[0]};
  0            
520 0           my @required = @{$_[1]};
  0            
521              
522 0           foreach (@required) {
523 0 0         if (!$args{$_}) {
524 0           croak("ERROR: Missing required param: $_")
525             }
526             }
527             }
528              
529             sub _fetchCSRF {
530 0     0     my ($ctx) = @_;
531              
532 0           my $url = _buildURL($ctx, 'login', 'whoAmI.api');
533              
534 0 0         print "CRSF " . $url . "\n" if $$ctx{debug};
535              
536 0           my $req = GET $url;
537 0           $req->content_type('application/json');
538              
539 0 0         if (!$$ctx{isGuest}) {
540 0           $req->authorization_basic($$ctx{auth}{'login'}, $$ctx{auth}{'password'});
541             }
542              
543 0           my $response = $$ctx{userAgent}->request($req);
544              
545 0 0         if ($response->is_error) {
546 0           croak($response->status_line . '\n' . $response->decoded_content);
547             }
548              
549 0   0       my $json_obj = JSON->new->utf8->decode($response->content)
550             || croak("ERROR: Unable to decode JSON.\n$url\n");
551              
552 0           return $$json_obj{'CSRF'};
553             }
554              
555             sub _getServerContext {
556 0     0     my %args = @_;
557              
558             #allow baseUrl as environment variable
559 0   0       $args{'-baseUrl'} = $args{'-baseUrl'} || $ENV{LABKEY_URL};
560              
561 0           my @required = ('-containerPath', '-baseUrl');
562 0           _checkRequiredParams(\%args, \@required);
563              
564             #if no machine supplied, extract domain from baseUrl
565 0 0         if (!$args{'-machine'}) {
566 0           $args{'-machine'} = URI->new($args{'-baseUrl'})->host;
567             }
568              
569 0           my $is_guest;
570             my $lk_config;
571 0   0       my $netrc_file = $args{-netrcFile} || $ENV{LABKEY_NETRC};
572              
573 0 0         if ($args{'-loginAsGuest'}) {
574 0           $is_guest = 1;
575             }
576             else {
577 0           $lk_config = _readrc($args{-machine}, $netrc_file);
578 0           $is_guest = 0;
579             }
580              
581             my $ctx = {
582             auth => $lk_config,
583             baseUrl => $args{'-baseUrl'},
584             containerPath => $args{'-containerPath'},
585             isGuest => $is_guest,
586 0   0       userAgent => $args{'-useragent'} || _createUserAgent(%args),
587             };
588              
589 0 0         if ($args{-debug}) {
590 0           $$ctx{debug} = 1;
591             }
592              
593 0           my $csrfHeader = "X-LABKEY-CSRF";
594              
595 0 0         if (!$$ctx{userAgent}->default_header($csrfHeader)) {
596 0           $$ctx{userAgent}->default_header($csrfHeader => _fetchCSRF($ctx));
597             }
598              
599 0           return $ctx;
600             }
601              
602             =pod
603              
604             =head1 ENVIORNMENT VARIABLES
605              
606             =over 4
607              
608             =item *
609             The 'LABKEY_URL' environment variable can be used instead of supplying a '-baseUrl' param.
610              
611             =item *
612             The 'LABKEY_NETRC' environment variable can be used to specify an alternate location of a netrc file, if not in the user's home directory.
613              
614             =back
615              
616             =head1 AUTHOR
617              
618             LabKey C
619              
620             =head1 CONTRIBUTING
621              
622             Send comments, suggestions and bug reports to:
623              
624             L
625              
626              
627             =head1 COPYRIGHT
628            
629             Copyright (c) 2010 Ben Bimber
630             Copyright (c) 2011-2018 LabKey Corporation
631              
632             Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
633              
634             =head1 SEE ALSO
635              
636             The LabKey client APIs are described in greater detail here:
637             https://www.labkey.org/Documentation/wiki-page.view?name=viewAPIs
638              
639             Support questions should be directed to the LabKey forum:
640             https://www.labkey.org/home/Server/Forum/announcements-list.view
641              
642             =cut
643              
644             1;
645