File Coverage

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

    Warnings:

    \n
      \n";
    1235 0           $warning .= "
  • $_
  • \n" foreach @warnings;
    1236 0           $warning .= "\n";
    1237             }
    1238            
    1239 0           my $body = <
    1240             $code $text
    1241            

    $code $text

    1242             $error
    1243             $warning
    1244            
    1245             END_BODY
    1246            
    1247 0           $Web::DataService::FOUNDATION->set_content_type($outer, 'text/html');
    1248 0           $Web::DataService::FOUNDATION->set_header($outer, 'Content-Disposition' => 'inline');
    1249 0           $Web::DataService::FOUNDATION->set_status($outer, $code);
    1250 0           $Web::DataService::FOUNDATION->set_body($outer, $body);
    1251             }
    1252             }
    1253              
    1254              
    1255             1;