File Coverage

lib/Web/DataService/Execute.pm
Criterion Covered Total %
statement 12 337 3.5
branch 0 232 0.0
condition 0 164 0.0
subroutine 4 23 17.3
pod 0 12 0.0
total 16 768 2.0


line stmt bran cond sub pod time code
1             #
2             # Web::DataService::Execute
3             #
4             # This module provides a role that is used by 'Web::DataService'. It implements
5             # routines for executing requests.
6             #
7             # Author: Michael McClennen
8              
9 2     2   18 use strict;
  2         4  
  2         103  
10              
11             package Web::DataService::Execute;
12              
13 2     2   14 use Carp 'croak';
  2         4  
  2         131  
14 2     2   15 use Scalar::Util qw(reftype weaken);
  2         4  
  2         94  
15              
16 2     2   12 use Moo::Role;
  2         4  
  2         18  
17              
18              
19              
20             # new_request ( outer, attrs )
21             #
22             # Generate a new request object, using the given attributes. $outer should be
23             # a reference to an "outer" request object that was generated by the
24             # underlying framework (i.e. Dancer or Mojolicious) or undef if there is
25             # none.
26              
27             sub new_request {
28              
29 0     0 0   my ($ds, $outer, $attrs) = @_;
30            
31             # First check the arguments to this method.
32            
33 0 0 0       croak "new_request: second argument must be a hashref\n"
34             if defined $attrs && ref $attrs ne 'HASH';
35            
36 0   0       $attrs ||= {};
37            
38             # If this was called as a class method rather than as an instance method,
39             # then call 'select' to figure out the appropriate data service.
40            
41 0 0         unless ( ref $ds eq 'Web::DataService' )
42             {
43 0           $ds = Web::DataService->select($outer);
44             }
45            
46             # Grab the request parameters from the foundation plugin.
47            
48 0           my $request_params = $Web::DataService::FOUNDATION->get_params($outer);
49            
50             # If "path" was not specified as an attribute, determine it from the request
51             # parameters and path.
52            
53 0 0         unless ( defined $attrs->{path} )
54             {
55 0           my $request_path = $Web::DataService::FOUNDATION->get_request_path($outer);
56            
57 0           $attrs->{path} = $ds->_determine_path($request_path, $request_params);
58             }
59            
60             # Now set the other required attributes, and create an object to represent
61             # this request.
62            
63 0           $attrs->{outer} = $outer;
64 0           $attrs->{ds} = $ds;
65 0   0       $attrs->{http_method} = $Web::DataService::FOUNDATION->get_http_method($outer) || 'UNKNOWN';
66            
67 0           my $request = Web::DataService::Request->new($attrs);
68            
69             # Make sure that the outer object is linked back to this request object.
70             # The link from the "inner" object to the "outer" must be weakened,
71             # so that garbage collection works properly.
72            
73 0 0         weaken($request->{outer}) if ref $request->{outer};
74 0           $Web::DataService::FOUNDATION->store_inner($outer, $request);
75            
76             # Return the new request object.
77            
78 0           return $request;
79             }
80              
81              
82             # _determine_path ( url_path, params )
83             #
84             # Given the request URL path and parameters, determine what the request path
85             # should be.
86              
87             sub _determine_path {
88            
89 0     0     my ($ds, $request_path, $request_params) = @_;
90            
91             # If the special parameter 'path' is active, then we determine the result
92             # from its value. If this parameter was not specified in the request, it
93             # defaults to ''.
94            
95 0 0         if ( my $path_param = $ds->{special}{path} )
    0          
96             {
97 0   0       my $path = $request_params->{$path_param} // '';
98 0           return $path;
99             }
100            
101             # Otherwise, we use the request path. In this case, if the data service
102             # has a path regexp, use it to trim the path.
103            
104             elsif ( defined $request_path )
105             {
106 0 0 0       if ( defined $ds->{path_re} && $request_path =~ $ds->{path_re} )
107             {
108 0   0       return $1 // '';
109             }
110            
111             else
112             {
113 0           return $request_path;
114             }
115             }
116            
117             # Otherwise, return the empty string.
118            
119             else
120             {
121 0           return '';
122             }
123             }
124              
125              
126             # handle_request ( request )
127             #
128             # Generate a new request object, match it to a data service node, and then execute
129             # it. This is a convenience routine.
130              
131             sub handle_request {
132              
133 0     0 0   my ($ds, $outer, $attrs) = @_;
134            
135             # If this was called as a class method rather than as an instance method,
136             # then call 'select' to figure out the appropriate data service.
137            
138 0 0         unless ( ref $ds eq 'Web::DataService' )
139             {
140 0           $ds = Web::DataService->select($outer);
141             }
142            
143             # Generate a new request object, then execute it.
144            
145 0           my $request = $ds->new_request($outer, $attrs);
146 0           return $ds->execute_request($request);
147             }
148              
149              
150             # execute_request ( request )
151             #
152             # Execute a request. Depending upon the request path, it may either be
153             # interpreted as a request for documentation or a request to execute some
154             # operation and return a result.
155              
156             sub execute_request {
157            
158 0     0 0   my ($ds, $request) = @_;
159            
160 0           my $path = $request->node_path;
161 0           my $format = $request->output_format;
162            
163             # Fetch the request method and the hash of allowed methods for this node. If none were
164             # specified, default to GET and HEAD.
165            
166 0           my $http_method = $request->http_method;
167 0   0       my $allow_method = $ds->node_attr($request, 'allow_method') || { GET => 1, HEAD => 1 };
168            
169             # If this was called as a class method rather than as an instance method,
170             # then call 'select' to figure out the appropriate data service.
171            
172 0 0         unless ( ref $ds eq 'Web::DataService' )
173             {
174 0           $ds = Web::DataService->select($request->outer);
175             }
176            
177             # Now that we have selected a data service instance, check to see if this
178             # program is in diagnostic mode. If so, then divert this request to the
179             # module Web::DataService::Diagnostic, and then exit the program when it
180             # is done.
181            
182 0 0         if ( Web::DataService->is_mode('diagnostic') )
183             {
184 0           $ds->diagnostic_request($request);
185 0           exit;
186             }
187            
188             # If the request HTTP method was 'OPTIONS', then return a list of methods
189             # allowed for this node path.
190            
191 0 0         if ( $http_method eq 'OPTIONS' )
192             {
193 0 0         my @methods = ref $allow_method eq 'HASH' ? keys %$allow_method : @Web::DataService::DEFAULT_METHODS;
194            
195 0           $ds->_set_cors_header($request);
196 0           $ds->_set_response_header($request, 'Access-Control-Allow-Methods', join(',', @methods));
197 0           return;
198             }
199            
200             # Otherwise, this is a standard request. If a 'before_execute_hook' was
201             # defined for this request, call it now.
202            
203             $ds->_call_hooks($path, 'before_execute_hook', $request)
204 0 0         if $ds->{hook_enabled}{before_execute_hook};
205            
206             # If the request has been tagged as an invalid path, then return a 404 error
207             # right away.
208            
209 0 0         die "404\n" if $request->{is_invalid_request};
210            
211             # If the request has been tagged as a "documentation path", then show the
212             # documentation. The only allowed methods for documentation are GET and HEAD.
213            
214 0 0 0       if ( $request->{is_node_path} && $request->{is_doc_request} && $ds->has_feature('documentation') )
    0 0        
    0 0        
      0        
215             {
216 0 0 0       unless ( $http_method eq 'GET' || $http_method eq 'HEAD' )
217             {
218 0           die "405 Method Not Allowed\n";
219             }
220            
221 0           return $ds->generate_doc($request);
222             }
223            
224             # If the 'is_file_path' attribute is set, we should be sending a file. Figure out the path
225             # and send it. We don't currently allow uploading files, so the only allowed methods are GET
226             # and HEAD.
227            
228             elsif ( $request->{is_file_path} && $ds->has_feature('send_files') )
229             {
230 0 0 0       unless ( $http_method eq 'GET' || $http_method eq 'HEAD' )
231             {
232 0           die "405 Method Not Allowed\n";
233             }
234            
235 0           return $ds->send_file($request);
236             }
237            
238             # If the selected node has an operation, execute it and return the result. But we first have
239             # to check if the request method is allowed.
240            
241             elsif ( $request->{is_node_path} && $ds->node_has_operation($path) )
242             {
243             # Always allow HEAD if GET is allowed. But otherwise reject any request that doesn't have
244             # an allowed method.
245            
246 0 0         my $check_method = $http_method eq 'HEAD' ? 'GET' : $http_method;
247            
248 0 0 0       unless ( $allow_method->{$http_method} || $allow_method->{$check_method} )
249             {
250 0           die "405 Method Not Allowed\n";
251             }
252            
253             # Almost all requests will go through this branch of the code. This leads to the actual
254             # execution of data service operations.
255            
256 0           $ds->configure_request($request);
257 0           return $ds->generate_result($request);
258             }
259            
260             # If the request cannot be satisfied in any of these ways, then return a 404 error.
261            
262 0           die "404\n";
263             }
264              
265              
266             # send_file ( request )
267             #
268             # Send a file using the attributes specified in the request node.
269              
270             sub send_file {
271              
272 0     0 0   my ($ds, $request) = @_;
273            
274 0 0         die "404\n" if $request->{is_invalid_request};
275            
276 0           my $rest_path = $request->{rest_path};
277 0           my $file_dir = $ds->node_attr($request, 'file_dir');
278 0           my $file_path;
279            
280             # How we handle this depends upon whether 'file_dir' or 'file_path' was
281             # set. With 'file_dir', an empty file name will always return a 404
282             # error, since the only other logical response would be a list of the base
283             # directory and we don't want to provide that for security reasons.
284            
285 0 0         if ( $file_dir )
286             {
287 0 0 0       die "404\n" unless defined $rest_path && $rest_path ne '';
288            
289             # Concatenate the path components together, using the foundation plugin so
290             # that this is done in a file-system-independent manner.
291            
292 0           $file_path = $Web::DataService::FOUNDATION->file_path($file_dir, $rest_path);
293             }
294            
295             # Otherwise, $rest_path must be empty or else we send back a 404 error.
296            
297             else
298             {
299 0 0 0       die "404\n" if defined $rest_path && $rest_path ne '';
300            
301 0           $file_path = $ds->node_attr($request, 'file_path');
302             }
303            
304             # If this file does not exist, return a 404 error. This is necessary so
305             # that the error handling will by done by Web::DataService rather than by
306             # Dancer. If the file exists but is not readable, return a 500 error.
307             # This is not a permission error, it is an internal server error.
308            
309 0 0         unless ( $Web::DataService::FOUNDATION->file_readable($file_path) )
310             {
311 0 0         die "500" if $Web::DataService::FOUNDATION->file_exists($file_path);
312 0           die "404\n"; # otherwise
313             }
314            
315             # Otherwise, send the file.
316            
317 0           return $Web::DataService::FOUNDATION->send_file($request->outer, $file_path);
318             }
319              
320              
321             # node_has_operation ( path )
322             #
323             # If this class has both a role and a method defined, then return the method
324             # name. Return undefined otherwise. This method can be used to determine
325             # whether a particular path is valid for executing a data service operation.
326              
327             sub node_has_operation {
328            
329 0     0 0   my ($ds, $path) = @_;
330            
331 0           my $role = $ds->node_attr($path, 'role');
332 0           my $method = $ds->node_attr($path, 'method');
333            
334 0 0 0       return $method if $role && $method;
335             }
336              
337              
338             # configure_request ( request )
339             #
340             # Determine the attributes necessary for executing the data service operation
341             # corresponding to the specified request.
342              
343             sub configure_request {
344            
345 0     0 0   my ($ds, $request) = @_;
346            
347 0           my $path = $request->node_path;
348            
349 0 0 0       die "404\n" if $request->{is_invalid_request} || $ds->node_attr($path, 'disabled');
350            
351 0           $request->{_configured} = 1;
352            
353             # If we are in 'one request' mode, initialize this request's primary
354             # role. If we are not in this mode, then all of the roles will have
355             # been previously initialized.
356            
357 0 0         if ( $Web::DataService::ONE_REQUEST )
358             {
359 0           my $role = $ds->node_attr($path, 'role');
360 0           $ds->initialize_role($role);
361             }
362            
363             # If a before_config_hook was specified for this node, call it now.
364            
365             $ds->_call_hooks($path, 'before_config_hook', $request)
366 0 0         if $ds->{hook_enabled}{before_config_hook};
367            
368             # Get the raw parameters for this request, if they have not already been gotten.
369            
370 0   0       $request->{raw_params} //= $Web::DataService::FOUNDATION->get_params($request);
371            
372             # Check to see if there is a ruleset corresponding to this path. If
373             # so, then validate the parameters according to that ruleset.
374            
375 0           my $rs_name = $ds->node_attr($path, 'ruleset');
376            
377 0   0       $rs_name //= $ds->determine_ruleset($path);
378            
379 0 0         if ( $rs_name )
380             {
381 0           my $context = { ds => $ds, request => $request };
382            
383 0           my $result = $ds->{validator}->check_params($rs_name, $context, $request->{raw_params});
384            
385 0 0         if ( $result->errors )
    0          
386             {
387 0           die $result;
388             }
389            
390             elsif ( $result->warnings )
391             {
392 0           $request->add_warning($result->warnings);
393             }
394            
395 0           $request->{clean_params} = $result->values;
396 0           $request->{valid} = $result;
397 0           $request->{ruleset} = $rs_name;
398            
399 0 0         if ( $ds->debug )
400             {
401 0           my $dsname = $ds->name;
402 0           print STDERR "---------------\nOperation $dsname '$path'\n";
403 0           foreach my $p ( $result->keys )
404             {
405 0           my $value = $result->value($p);
406 0 0         $value = join(', ', @$value) if ref $value eq 'ARRAY';
407 0   0       $value ||= '[ NO GOOD VALUES FOUND ]';
408 0           print STDERR "$p = $value\n";
409             }
410             }
411             }
412            
413             # Otherwise, just pass the raw parameters along with no validation or
414             # processing.
415            
416             else
417             {
418 0 0         print STDERR "No ruleset could be determined for path '$path'\n" if $ds->debug;
419 0           $request->{valid} = undef;
420 0           $request->{clean_params} = $request->{raw_params};
421             }
422            
423             # Now that the parameters have been processed, we can configure all of
424             # the settings that might be specified or affected by parameter values:
425            
426             # If the output format is not already set, then try to determine what
427             # it should be.
428            
429 0 0         unless ( $request->output_format )
430             {
431             # If the special parameter 'format' is enabled, check to see if a
432             # value for that parameter was given.
433            
434 0           my $format;
435 0           my $format_param = $ds->{special}{format};
436            
437 0 0         if ( $format_param )
438             {
439 0           $format = $request->{clean_params}{$format_param};
440             }
441            
442             # If we still don't have a format, and there is a default format
443             # specified for this path, use that.
444            
445 0   0       $format //= $ds->node_attr($path, 'default_format');
446            
447             # Otherwise, use the first format defined.
448            
449 0   0       $format //= ${$ds->{format_list}}[0];
  0            
450            
451             # If we have successfully determined a format, then set the result
452             # object's output format attribute.
453            
454 0 0         $request->output_format($format) if $format;
455             }
456            
457             # Next, determine the result limit and offset, if any. If the special
458             # parameter 'limit' is active, then see if this request included it.
459             # If we couldn't get a parameter value, see if a default limit was
460             # specified for this node or for the data service as a whole.
461            
462 0   0       my $limit_value = $request->special_value('limit') //
463             $ds->node_attr($path, 'default_limit');
464            
465 0 0         $request->result_limit($limit_value) if defined $limit_value;
466            
467             # If the special parameter 'offset' is active, then see if this result
468             # included it.
469            
470 0           my $offset_value = $request->special_value('offset');
471            
472 0 0         $request->result_offset($offset_value) if defined $offset_value;
473            
474             # Determine whether we should show the optional header information in
475             # the result.
476            
477 0   0       my $header_value = $request->special_value('header') //
478             $ds->node_attr($path, 'default_header');
479            
480 0 0         $request->display_header($header_value) if defined $header_value;
481            
482 0   0       my $source_value = $request->special_value('datainfo') //
483             $ds->node_attr($path, 'default_datainfo');
484            
485 0 0         $request->display_datainfo($source_value) if defined $source_value;
486            
487 0   0       my $count_value = $request->special_value('count') //
488             $ds->node_attr($path, 'default_count');
489            
490 0 0         $request->display_counts($count_value) if defined $count_value;
491            
492 0   0       my $output_linebreak = $request->special_value('linebreak') ||
493             $ds->node_attr($path, 'default_linebreak') || 'crlf';
494            
495 0           $request->output_linebreak($output_linebreak);
496            
497 0           my $save_specified = $request->special_given('save');
498 0   0       my $save_value = $request->special_value('save') || '';
499            
500 0 0         if ( $save_specified )
501             {
502 0 0         if ( $save_value =~ qr{ ^ (?: no | off | 0 | false ) $ }xsi )
503             {
504 0           $request->save_output(0);
505             }
506            
507             else
508             {
509 0           $request->save_output(1);
510 0 0 0       $request->save_filename($save_value) if $save_value ne '' &&
511             $save_value !~ qr{ ^ (?: yes | on | 1 | true ) $ }xsi;
512             }
513             }
514            
515             # Determine which vocabulary to use. If the special parameter 'vocab' is
516             # active, check that first.
517            
518 0           my $vocab_value = $request->special_value('vocab');
519            
520 0 0         $request->output_vocab($vocab_value) if defined $vocab_value;
521            
522 0           my $a = 1; # we can stop here when debugging
523             }
524              
525              
526             # generate_result ( request )
527             #
528             # Execute the operation corresponding to the attributes of the node selected
529             # by the given request, and return the resulting data. This routine is, in
530             # many ways, the core of this entire project.
531              
532             sub generate_result {
533            
534 0     0 0   my ($ds, $request) = @_;
535            
536             croak "generate_result: you must first call the method 'configure'\n"
537 0 0         unless $request->{_configured};
538            
539 0           my $path = $request->node_path;
540 0           my $format = $request->output_format;
541            
542 0           my $method = $ds->node_attr($path, 'method');
543 0           my $arg = $ds->node_attr($path, 'arg');
544            
545             # First determine the class that corresponds to this request's primary role
546             # and bless the request into that class.
547            
548 0           my $role = $ds->node_attr($request, 'role');
549 0           bless $request, $ds->execution_class($role);
550            
551             # If a before_setup_hook is defined for this path, call it.
552            
553             $ds->_call_hooks($path, 'before_setup_hook', $request)
554 0 0         if $ds->{hook_enabled}{before_setup_hook};
555            
556             # First check to make sure that the specified format is valid for the
557             # specified path.
558            
559 0 0         unless ( $ds->valid_format_for($path, $format) )
560             {
561 0           die "415\n";
562             }
563            
564             # defined $format && ref $ds->{format}{$format} &&
565             # ! $ds->{format}{$format}{disabled} &&
566             # $attrs->{allow_format}{$format} )
567            
568             # Then we need to make sure that an output vocabulary is selected. If no
569             # vocabulary was explicitly specified, then try the default for the
570             # selected format. As a backup, we use the first vocabulary defined for
571             # the data service, which will be the default vocabulary if none were
572             # explicitly defined.
573            
574 0 0         unless ( my $vocab_value = $request->output_vocab )
575             {
576             $vocab_value = $ds->{format}{$format}{default_vocab} ||
577 0   0       $ds->{vocab_list}[0];
578            
579 0           $request->output_vocab($vocab_value);
580             }
581            
582             # Now that we know the format, we can set the response headers.
583            
584 0           $ds->_set_cors_header($request);
585 0           $ds->_set_content_type($request);
586            
587             # If the format indicates that the output should be returned as an
588             # attachment (which tells the browser to save it to disk), note this fact.
589            
590 0           my $save_flag = $request->save_output;
591 0           my $disp = $ds->{format}{$format}{disposition};
592            
593 0 0 0       if ( defined $save_flag && $save_flag eq '0' )
    0 0        
      0        
594             {
595             #$ds->_set_content_disposition($request, 'inline');
596 0 0         $ds->_set_content_type($request, 'text/plain') if $ds->{format}{$format}{is_text};
597 0           $request->{content_type_is_text} = 1;
598             }
599            
600             elsif ( ( defined $disp && $disp eq 'attachment' ) ||
601             $save_flag )
602             {
603 0           $ds->_set_content_disposition($request, 'attachment', $request->save_filename);
604             }
605            
606             # Then set up the output. This involves constructing a list of
607             # specifiers that indicate which fields will be included in the output
608             # and how they will be processed.
609            
610 0           $ds->_setup_output($request);
611            
612             # If a summary block has been specified for this request, configure it as
613             # well.
614            
615 0 0         if ( my $summary_block = $ds->node_attr($request, 'summary') )
616             {
617 0 0         if ( $ds->configure_block($request, $summary_block) )
618             {
619 0           $request->{summary_field_list} = $request->{block_field_list}{$summary_block};
620             }
621             else
622             {
623 0           $request->add_warning("Summary block '$summary_block' not found");
624             }
625             }
626            
627             # If a before_operation_hook is defined for this path, call it.
628             # Also check for post_configure_hook, for backward compatibility.
629            
630             $ds->_call_hooks($path, 'post_configure_hook', $request)
631 0 0         if $ds->{hook_enabled}{post_configure_hook};
632            
633             $ds->_call_hooks($path, 'before_operation_hook', $request)
634 0 0         if $ds->{hook_enabled}{before_operation_hook};
635            
636             # Prepare to time the query operation.
637            
638 0           my (@starttime) = Time::HiRes::gettimeofday();
639            
640             # Now execute the query operation. This is the central step of this
641             # entire routine; everything before and after is in support of this call.
642            
643 0           $request->$method($arg);
644            
645             # Determine how long the query took.
646            
647 0           my (@endtime) = Time::HiRes::gettimeofday();
648 0           $request->{elapsed} = Time::HiRes::tv_interval(\@starttime, \@endtime);
649            
650             # If a before_output_hook is defined for this path, call it.
651            
652             $ds->_call_hooks($path, 'before_output_hook', $request)
653 0 0         if $ds->{hook_enabled}{before_output_hook};
654            
655             # Then we use the output configuration and the result of the query
656             # operation to generate the actual output. How we do this depends
657             # upon how the operation method chooses to return its data. It must
658             # set one of the following fields in the request object, as described:
659             #
660             # main_data A scalar, containing data which is to be
661             # returned as-is without further processing.
662             #
663             # main_record A hashref, representing a single record to be
664             # returned according to the output format.
665             #
666             # main_result A list of hashrefs, representing multiple
667             # records to be returned according to the output
668             # format.
669             #
670             # main_sth A DBI statement handle, from which all
671             # records that can be read should be returned
672             # according to the output format.
673             #
674             # It is okay for main_result and main_sth to both be set, in which
675             # case the records in the former will be sent first and then the
676             # latter will be read.
677            
678 0 0 0       if ( ref $request->{main_record} )
    0          
    0          
679             {
680 0           return $ds->_generate_single_result($request);
681             }
682            
683             elsif ( ref $request->{main_sth} or ref $request->{main_result} )
684             {
685             my $threshold = $ds->node_attr($path, 'streaming_threshold')
686 0 0         unless $request->{do_not_stream};
687            
688             # If the result set requires processing before output, then call
689             # _generate_processed_result. Otherwise, call
690             # _generate_compound_result. One of the conditions that can cause
691             # this to happen is if record counts are requested and generating them
692             # requires processing (i.e. because a 'check' rule was encountered).
693            
694 0 0 0       $request->{preprocess} = 1 if $request->display_counts && $request->{process_before_count};
695            
696 0 0         if ( $request->{preprocess} )
697             {
698 0           return $ds->_generate_processed_result($request, $threshold);
699             }
700            
701             else
702             {
703 0           return $ds->_generate_compound_result($request, $threshold);
704             }
705             }
706            
707             elsif ( defined $request->{main_data} )
708             {
709 0           return $request->{main_data};
710             }
711            
712             # If none of these fields are set, then the result set is empty.
713            
714             else
715             {
716 0           return $ds->_generate_empty_result($request);
717             }
718             }
719              
720              
721             # _call_hooks ( path, hook, request )
722             #
723             # If the specified hook has been defined for the specified path, call each of
724             # the defined values. If the value is a code reference, call it with the
725             # request as the only parameter. If it is a string, call it as a method of
726             # the request object.
727              
728             sub _call_hooks {
729            
730 0     0     my ($ds, $path, $hook_name, $request, @args) = @_;
731              
732             # Look up the list of hooks, if any, defined for this node.
733            
734 0   0       my $hook_list = $ds->node_attr($path, $hook_name) || return;
735            
736             # Then call each hook in turn. The return value will be the return value of the hook last
737             # called, which will be the one that is defined furthest down in the hierarchy.
738            
739 0           foreach my $hook ( @$hook_list )
740             {
741 0 0         if ( ref $hook eq 'CODE' )
    0          
742             {
743 0           &$hook($request, @args);
744             }
745            
746             elsif ( defined $hook )
747             {
748 0           $request->$hook(@args);
749             }
750             }
751             }
752              
753              
754             sub _call_hook_list {
755            
756 0     0     my ($ds, $hook_list, $request, @args) = @_;
757            
758 0           foreach my $hook ( @$hook_list )
759             {
760 0 0         if ( ref $hook eq 'CODE' )
    0          
761             {
762 0           &$hook($request, @args);
763             }
764            
765             elsif ( defined $hook )
766             {
767 0           $request->$hook(@args);
768             }
769             }
770             }
771              
772              
773             sub _set_cors_header {
774            
775 0     0     my ($ds, $request, $arg) = @_;
776            
777             # If this is a public-access data service, we add a universal CORS header.
778             # At some point we need to add provision for authenticated access.
779            
780 0 0 0       if ( (defined $arg && $arg eq '*') || $ds->node_attr($request, 'public_access') )
      0        
781             {
782 0           $Web::DataService::FOUNDATION->set_header($request->outer, "Access-Control-Allow-Origin", "*");
783             }
784             }
785              
786              
787             sub _set_response_header {
788              
789 0     0     my ($ds, $request, $header, $value) = @_;
790            
791             # Set the specified response header, with the given value.
792            
793 0           $Web::DataService::FOUNDATION->set_header($request->outer, $header, $value);
794             }
795              
796              
797             sub _set_content_type {
798              
799 0     0     my ($ds, $request, $ct) = @_;
800            
801             # If the content type was not explicitly given, choose it based on the
802             # output format.
803            
804 0 0         unless ( $ct )
805             {
806 0           my $format = $request->output_format;
807 0   0       $ct = $ds->{format}{$format}{content_type} || 'text/plain';
808             }
809            
810 0           $Web::DataService::FOUNDATION->set_content_type($request->outer, $ct);
811             }
812              
813              
814             sub _set_content_disposition {
815            
816 0     0     my ($ds, $request, $disp, $filename) = @_;
817            
818             # If we were given a disposition of 'inline', then set that.
819            
820 0 0         if ( $disp eq 'inline' )
821             {
822 0           $Web::DataService::FOUNDATION->set_header($request->outer, 'Content-Disposition' => 'inline');
823 0           return;
824             }
825            
826             # If we weren't given an explicit filename, check to see if one was set
827             # for this node.
828            
829 0   0       $filename //= $ds->node_attr($request, 'default_save_filename');
830            
831             # If we still don't have a filename, return without doing anything.
832            
833 0 0         return unless $filename;
834            
835             # Otherwise, set the appropriate header. If the filename does not already
836             # include a suffix, add the format.
837            
838 0 0         unless ( $filename =~ qr{ [^.] [.] \w+ $ }xs )
839             {
840 0           $filename .= '.' . $request->output_format;
841             }
842            
843 0           $Web::DataService::FOUNDATION->set_header($request->outer, 'Content-Disposition' =>
844             qq{attachment; filename="$filename"});
845             }
846              
847              
848             # valid_format_for ( path, format )
849             #
850             # Return true if the specified format is valid for the specified path, false
851             # otherwise.
852              
853             sub valid_format_for {
854            
855 0     0 0   my ($ds, $path, $format) = @_;
856            
857 0           my $allow_format = $ds->node_attr($path, 'allow_format');
858 0 0         return unless ref $allow_format eq 'HASH';
859 0           return $allow_format->{$format};
860             }
861              
862              
863             # determine_ruleset ( )
864             #
865             # Determine the ruleset that should apply to this request. If a ruleset name
866             # was explicitly specified for the request path, then use that if it is
867             # defined or throw an exception if not. Otherwise, try the path with slashes
868             # turned into commas and the optional ruleset_prefix applied.
869              
870             sub determine_ruleset {
871            
872 0     0 0   my ($ds, $path) = @_;
873            
874 0           my $validator = $ds->{validator};
875 0           my $ruleset = $ds->node_attr($path, 'ruleset');
876            
877             # If a ruleset name was explicitly given, then use that or throw an
878             # exception if not defined.
879            
880 0 0 0       if ( defined $ruleset && $ruleset ne '' )
881             {
882 0 0         croak "unknown ruleset '$ruleset' for path $path"
883             unless $validator->ruleset_defined($ruleset);
884            
885 0           return $ruleset;
886             }
887            
888             # If the ruleset was explicitly specified as '', do not process the
889             # parameters for this path.
890            
891 0 0         return if defined $ruleset;
892            
893             # If the path is either empty or the root node '/', likewise return false.
894            
895 0 0 0       return unless defined $path && $path ne '' && $path ne '/';
      0        
896            
897             # Otherwise, try the path with / replaced by :. If that is not defined,
898             # then return empty. The parameters for this path will not be processed.
899            
900 0           $path =~ s{/}{:}g;
901            
902             $path = $ds->{ruleset_prefix} . $path
903 0 0 0       if defined $ds->{ruleset_prefix} && $ds->{ruleset_prefix} ne '';
904            
905 0 0         return $path if $validator->ruleset_defined($path);
906             }
907              
908              
909             # determine_output_names {
910             #
911             # Determine the output block(s) and/or map(s) that should be used for this
912             # request. If any output names were explicitly specified for the request
913             # path, then use them or throw an error if any are undefined. Otherwise, try
914             # the path with slashes turned into colons and either ':default' or
915             # ':default_map' appended.
916              
917             sub determine_output_names {
918              
919 0     0 0   my ($self) = @_;
920            
921 0           my $ds = $self->{ds};
922 0           my $path = $self->{path};
923 0 0         my @output_list = @{$self->{attrs}{output}} if ref $self->{attrs}{output} eq 'ARRAY';
  0            
924            
925             # If any output names were explicitly given, then check to make sure each
926             # one corresponds to a known block or set. Otherwise, throw an exception.
927            
928 0           foreach my $output_name ( @output_list )
929             {
930             croak "the string '$output_name' does not correspond to a defined output block or map"
931             unless ref $ds->{set}{$output_name} eq 'Web::DataService::Set' ||
932 0 0 0       ref $ds->{block}{$output_name} eq 'Web::DataService::Block';
933             }
934            
935             # Return the list.
936            
937 0           return @output_list;
938             }
939              
940              
941             # determine_output_format ( outer, inner )
942             #
943             # This method is called by the error reporting routine if we do not know the
944             # output format. We are given (possibly) both types of objects and need to
945             # determine the appropriate output format based on the data service
946             # configuration and the request path and parameters.
947             #
948             # This method need only return a value if that value is not 'html', because
949             # that is the default.
950              
951             sub determine_output_format {
952              
953 0     0 0   my ($ds, $outer, $inner) = @_;
954            
955             # If the data service has the feature 'format_suffix', then check the
956             # URL path. If no format is specified, we return the empty string.
957            
958 0 0         if ( $ds->{feature}{format_suffix} )
959             {
960 0           my $path = $Web::DataService::FOUNDATION->get_request_path($outer);
961            
962 0           $path =~ qr{ [.] ( [^.]+ ) $ }xs;
963 0   0       return $1 || '';
964             }
965            
966             # Otherwise, if the special parameter 'format' is enabled, check to see if
967             # a value for that parameter was given.
968            
969 0 0         if ( my $format_param = $ds->{special}{format} )
970             {
971             # If the parameters have already been validated, check the cleaned
972             # parameter values.
973            
974 0 0 0       if ( ref $inner && reftype $inner eq 'HASH' && $inner->{clean_params} )
      0        
975             {
976             return $inner->{clean_params}{$format_param}
977 0 0         if $inner->{clean_params}{$format_param};
978             }
979            
980             # Otherwise, check the raw parameter values.
981            
982             else
983             {
984 0           my $params = $Web::DataService::FOUNDATION->get_params($outer);
985            
986 0 0         return lc $params->{$format_param} if $params->{$format_param};
987             }
988             }
989            
990             # If no parameter value was found, see if we have identified a data
991             # service node for this request. If so, check to see if a default format
992             # was established.
993            
994 0 0 0       if ( ref $inner && $inner->isa('Web::DataService::Request') )
995             {
996 0           my $default_format = $ds->node_attr($inner, 'default_format');
997            
998 0 0         return $default_format if $default_format;
999             }
1000            
1001             # If we really can't tell, then return the empty string which will cause
1002             # the format to default to 'html'.
1003            
1004 0           return '';
1005             }
1006              
1007              
1008             my %CODE_STRING = ( 400 => "Bad Request",
1009             401 => "Authentication Required",
1010             404 => "Not Found",
1011             415 => "Invalid Media Type",
1012             422 => "Cannot be processed",
1013             500 => "Server Error" );
1014              
1015             # error_result ( error, request )
1016             #
1017             # Send an error response back to the client. This routine is designed to be
1018             # as flexible as possible about its arguments. At minimum, it only needs a
1019             # request object - either the one generated by the foundation framework or
1020             # the one generated by Web::DataService.
1021              
1022             sub error_result {
1023              
1024 0     0 0   my ($ds, $error, $request) = @_;
1025            
1026             # If we are in 'debug' mode, then print out the error message.
1027            
1028 0 0         if ( Web::DataService->is_mode('debug') )
1029             {
1030 0 0         unless ( defined $error )
    0          
    0          
    0          
    0          
1031             {
1032 0           Dancer::debug("CAUGHT UNKNOWN ERROR");
1033             }
1034            
1035 0           elsif ( ! ref $error )
1036             {
1037 0           Dancer::debug("CAUGHT ERROR: " . $error);
1038             }
1039            
1040 0           elsif ( $error->isa('HTTP::Validate::Result') )
1041             {
1042 0           Dancer::debug("CAUGHT HTTP::VALIDATE RESULT");
1043             }
1044            
1045 0           elsif ( $error->isa('Dancer::Exception::Base') )
1046             {
1047 0           Dancer::debug("CAUGHT ERROR: " . $error->message);
1048             }
1049            
1050 0           elsif ( $error->isa('Web::DataService::Exception') )
1051             {
1052 0           Dancer::debug("CAUGHT EXCEPTION: " . $error->{message});
1053             }
1054            
1055             else
1056             {
1057 0           Dancer::debug("CAUGHT OTHER ERROR");
1058             }
1059             }
1060            
1061             # Then figure out which kind of request object we have.
1062            
1063 0           my ($inner, $outer);
1064            
1065             # If we were given the 'inner' request object, we can retrieve the 'outer'
1066             # one from that.
1067            
1068 0 0 0       if ( ref $request && $request->isa('Web::DataService::Request') )
    0          
1069             {
1070 0           $inner = $request;
1071 0           $outer = $request->outer;
1072             }
1073            
1074             # If we were given the 'outer' object, ask the foundation framework to
1075             # tell us the corresponding 'inner' one.
1076            
1077             elsif ( defined $request )
1078             {
1079 0           $outer = $request;
1080 0           $inner = $Web::DataService::FOUNDATION->retrieve_inner($outer);
1081             }
1082            
1083             # Otherwise, ask the foundation framework to tell us the current request.
1084            
1085             else
1086             {
1087 0           $outer = $Web::DataService::FOUNDATION->retrieve_outer();
1088 0           $inner = $Web::DataService::FOUNDATION->retrieve_inner($outer);
1089             }
1090            
1091             # Get the proper data service instance from the inner request, in case we
1092             # were called as a class method.
1093            
1094 0 0 0       $ds = defined $inner && $inner->isa('Web::DataService::Request') ? $inner->ds
1095             : $Web::DataService::WDS_INSTANCES[0];
1096            
1097             # Next, try to determine the format of the result
1098            
1099 0           my $format;
1100 0 0 0       $format ||= $inner->output_format if $inner;
1101 0   0       $format ||= $ds->determine_output_format($outer, $inner);
1102            
1103 0           my ($code);
1104 0           my (@errors, @warnings, @cautions);
1105            
1106 0 0 0       if ( ref $inner && $inner->isa('Web::DataService::Request') )
1107             {
1108 0           @warnings = $inner->warnings;
1109 0           @errors = $inner->errors;
1110 0           @cautions = $inner->cautions;
1111             }
1112            
1113             # If the error is actually a response object from HTTP::Validate, then
1114             # extract the error and warning messages. In this case, the error code
1115             # should be "400 bad request".
1116            
1117 0 0         if ( ref $error eq 'HTTP::Validate::Result' )
    0          
    0          
    0          
1118             {
1119 0           push @errors, $error->errors;
1120 0           push @warnings, $error->warnings;
1121 0           $code = "400";
1122             }
1123            
1124             elsif ( ref $error eq 'Web::DataService::Exception' )
1125             {
1126 0 0         push @errors, $error->{message} if ! @errors;
1127 0           $code = $error->{code};
1128             }
1129            
1130             # If the error message begins with a 3-digit number, then that should be
1131             # used as the code and the rest of the message as the error text.
1132            
1133             elsif ( $error =~ qr{ ^ (\d\d\d) \s+ (.+) }xs )
1134             {
1135 0           $code = $1;
1136 0           my $msg = $2;
1137 0           $msg =~ s/\n$//;
1138 0           push @errors, $msg;
1139             }
1140            
1141             elsif ( $error =~ qr{ ^ (\d\d\d) }xs )
1142             {
1143 0           $code = $1;
1144            
1145 0 0         if ( $code eq '404' )
    0          
1146             {
1147 0           my $path = $Web::DataService::FOUNDATION->get_request_path($outer);
1148 0 0 0       if ( defined $path && $path ne '' )
1149             {
1150 0           push @errors, "The path '$path' was not found on this server.";
1151             }
1152            
1153             else
1154             {
1155 0           push @errors, "This request is invalid.";
1156             }
1157             }
1158            
1159             elsif ( $CODE_STRING{$code} )
1160             {
1161 0           push @errors, $CODE_STRING{$code};
1162             }
1163            
1164             else
1165             {
1166 0 0         push @errors, "Error" unless @errors;
1167             }
1168             }
1169            
1170             # Otherwise, this is an internal error and all that we should report to
1171             # the user (for security reasons) is that an error occurred. The actual
1172             # message is written to the server error log.
1173            
1174             else
1175             {
1176 0           $code = 500;
1177 0           print STDERR warn $error;
1178 0           @errors = "A server error occurred. Please contact the server administrator.";
1179             }
1180            
1181             # If we know the format and if the corresponding format class knows how to
1182             # generate error messages, then take advantage of that functionality.
1183            
1184 0 0         my $format_class = $ds->{format}{$format}{package} if $format;
1185            
1186 0 0 0       if ( $format_class && $format_class->can('emit_error') )
1187             {
1188 0           my $error_body = $format_class->emit_error($code, \@errors, \@warnings, \@cautions);
1189 0   0       my $content_type = $ds->{format}{$format}{content_type} || 'text/plain';
1190            
1191 0           $Web::DataService::FOUNDATION->set_content_type($outer, $content_type);
1192 0           $Web::DataService::FOUNDATION->set_header($outer, 'Content-Disposition' => 'inline');
1193 0           $Web::DataService::FOUNDATION->set_cors_header($outer, "*");
1194 0           $Web::DataService::FOUNDATION->set_status($outer, $code);
1195 0           $Web::DataService::FOUNDATION->set_body($outer, $error_body);
1196             }
1197            
1198             # Otherwise, generate a generic HTML response (we'll add template
1199             # capability later...)
1200            
1201             else
1202             {
1203 0   0       my $text = $CODE_STRING{$code} || 'Error';
1204 0           my $error = "
    \n";
1205 0           my $warning = '';
1206            
1207 0           $error .= "
  • $_
  • \n" foreach @errors;
    1208 0           $error .= "\n";
    1209            
    1210 0 0         shift @warnings unless $warnings[0];
    1211            
    1212 0 0         if ( @warnings )
    1213             {
    1214 0           $warning .= "

    Warnings:

    \n
      \n";
    1215 0           $warning .= "
  • $_
  • \n" foreach @warnings;
    1216 0           $warning .= "\n";
    1217             }
    1218            
    1219 0           my $body = <
    1220             $code $text
    1221            

    $code $text

    1222             $error
    1223             $warning
    1224            
    1225             END_BODY
    1226            
    1227 0           $Web::DataService::FOUNDATION->set_content_type($outer, 'text/html');
    1228 0           $Web::DataService::FOUNDATION->set_header($outer, 'Content-Disposition' => 'inline');
    1229 0           $Web::DataService::FOUNDATION->set_status($outer, $code);
    1230 0           $Web::DataService::FOUNDATION->set_body($outer, $body);
    1231             }
    1232             }
    1233              
    1234              
    1235             1;