File Coverage

lib/Web/DataService/Output.pm
Criterion Covered Total %
statement 76 762 9.9
branch 23 638 3.6
condition 5 362 1.3
subroutine 10 31 32.2
pod 0 19 0.0
total 114 1812 6.2


line stmt bran cond sub pod time code
1             #
2             # Web::DataService::Output
3             #
4             # This module provides a role that is used by 'Web::DataService'. It implements
5             # routines for configuring and generating data service output.
6             #
7             # Author: Michael McClennen
8              
9 2     2   15 use strict;
  2         4  
  2         80  
10              
11             package Web::DataService::Output;
12              
13 2     2   11 use Encode;
  2         5  
  2         171  
14 2     2   13 use Scalar::Util qw(reftype);
  2         4  
  2         87  
15 2     2   11 use Carp qw(carp croak);
  2         5  
  2         81  
16              
17 2     2   11 use Moo::Role;
  2         5  
  2         16  
18              
19              
20             sub define_output_map {
21            
22 0     0 0 0 goto \&Web::DataService::Set::define_set;
23             }
24              
25              
26             # define_block ( name, specification... )
27             #
28             # Define an output block with the specified name, using the given
29             # specification records.
30              
31             sub define_block {
32            
33 1     1 0 11 my $ds = shift;
34 1         3 my $name = shift;
35            
36             # Check to make sure that we were given a valid name.
37            
38 1 50       19 if ( ref $name )
    50          
39             {
40 0         0 croak "define_block: the first argument must be an output block name";
41             }
42            
43             elsif ( not $ds->valid_name($name) )
44             {
45 0         0 croak "define_block: invalid block name '$name'";
46             }
47            
48             # Make sure the block name is unique.
49            
50 1 50       6 if ( $ds->{block}{$name} )
51             {
52 0         0 my $location = $ds->{block_loc}{$name};
53 0         0 croak "define_block: '$name' was already defined at $location\n";
54             }
55            
56             else
57             {
58 1         12 my ($package, $filename, $line) = caller;
59 1         9 $ds->{block_loc}{$name} = "$filename at line $line";
60             }
61            
62             # Create a new block object.
63            
64 1         5 my $block = { name => $name,
65             include_list => [],
66             output_list => [] };
67            
68 1         17 $ds->{block}{$name} = bless $block, 'Web::DataService::Block';
69            
70             # Then process the records one by one. Make sure to throw an error if we
71             # find a record whose type is ambiguous or that is otherwise invalid. Each
72             # record gets put in a list that is stored under the section name.
73            
74 1         4 foreach my $item (@_)
75             {
76             # A scalar is interpreted as a documentation string.
77            
78 4 100       10 unless ( ref $item )
79             {
80 2         6 $ds->add_doc($block, $item);
81 2         6 next;
82             }
83            
84             # Any item that is not a hashref is an error.
85            
86 2 50       7 unless ( ref $item eq 'HASH' )
87             {
88 0         0 croak "the arguments to 'output_section' must be hashrefs or scalars";
89             }
90            
91             # Check the output record to make sure it was specified correctly.
92            
93 2         7 my ($type) = $ds->check_output_record($item);
94            
95             # If the type is 'field', then any subsequent documentation strings
96             # will be added to that record.
97            
98 2 50       11 $ds->add_doc($block, $item) if $type eq 'output';
99            
100             # Add the record to the appropriate list(s).
101            
102 2 50       6 if ( $type eq 'include' )
103             {
104 0         0 push @{$ds->{block}{$name}{include_list}}, $item;
  0         0  
105             }
106            
107 2         4 push @{$ds->{block}{$name}{output_list}}, $item;
  2         7  
108             }
109            
110 1         4 $ds->process_doc($block);
111             }
112              
113              
114             our %OUTPUT_DEF = (output => 'type',
115             set => 'type',
116             select => 'type',
117             filter => 'type',
118             include => 'type',
119             check => 'type',
120             if_block => 'set',
121             not_block => 'set',
122             if_vocab => 'set',
123             not_vocab => 'set',
124             if_format => 'set',
125             not_format => 'set',
126             if_field => 'single',
127             not_field => 'single',
128             if_code => 'code',
129             dedup => 'single',
130             name => 'single',
131             value => 'single',
132             always => 'single',
133             text_join => 'single',
134             xml_join => 'single',
135             show_as_list => 'single',
136             data_type => 'single',
137             sub_record => 'single',
138             from => 'single',
139             from_each => 'single',
140             append => 'single',
141             code => 'code',
142             lookup => 'hash',
143             default => 'single',
144             split => 'regexp',
145             join => 'single',
146             tables => 'set',
147             disabled => 'single',
148             doc_string => 'single');
149              
150             our %SELECT_KEY = (select => 1, tables => 1, if_block => 1);
151              
152             our %FIELD_KEY = (dedup => 1, name => 1, value => 1, always => 1, sub_record => 1, if_field => 1,
153             not_field => 1, if_block => 1, not_block => 1, if_format => 1, not_format => 1,
154             if_vocab => 1, not_vocab => 1,
155             text_join => 1, xml_join => 1, doc_string => 1, show_as_list => 1, disabled => 1, undocumented => 1);
156              
157             our %PROC_KEY = (set => 1, check => 1, append => 1, from => 1, from_each => 1,
158             if_vocab => 1, not_vocab => 1, if_block => 1, not_block => 1,
159             if_format => 1, not_format => 1, if_field => 1, not_field => 1,
160             code => 1, lookup => 1, split => 1, join => 1, default => 1, disabled => 1);
161              
162             sub check_output_record {
163            
164 2     2 0 5 my ($ds, $record) = @_;
165            
166 2         4 my $type = '';
167            
168 2         6 foreach my $k (keys %$record)
169             {
170 2         4 my $v = $record->{$k};
171            
172 2 50       16 if ( $k =~ qr{ ^ (\w+) _ (name|value) $ }x )
    50          
    50          
    0          
    0          
    0          
    0          
    0          
173             {
174             croak "define_output: unknown format or vocab '$1' in '$k'"
175 0 0 0     0 unless defined $ds->{vocab}{$1} || defined $ds->{format}{$1};
176             }
177            
178             elsif ( ! defined $OUTPUT_DEF{$k} )
179             {
180 0         0 croak "define_output: unrecognized attribute '$k'";
181             }
182            
183             elsif ( $OUTPUT_DEF{$k} eq 'type' )
184             {
185 2 50       5 croak "define_output: you cannot have both attributes '$type' and '$k' in one record"
186             if $type;
187            
188 2         5 $type = $k;
189             }
190            
191             elsif ( $OUTPUT_DEF{$k} eq 'single' )
192             {
193 0 0       0 croak "define_output: the value of '$k' must be a scalar" if ref $v;
194             }
195            
196             elsif ( $OUTPUT_DEF{$k} eq 'set' )
197             {
198 0 0 0     0 croak "define_output: the value of '$k' must be an array ref or string"
199             if ref $v && reftype $v ne 'ARRAY';
200            
201 0 0       0 unless ( ref $v )
202             {
203 0         0 $record->{$k} = [ split(qr{\s*,\s*}, $v) ];
204             }
205             }
206            
207             elsif ( $OUTPUT_DEF{$k} eq 'code' )
208             {
209 0 0 0     0 croak "define_output: the value of '$k' must be a code ref"
210             unless ref $v && reftype $v eq 'CODE';
211             }
212            
213             elsif ( $OUTPUT_DEF{$k} eq 'hash' )
214             {
215 0 0 0     0 croak "define_output: the value of '$k' must be a hash ref"
216             unless ref $v && reftype $v eq 'HASH';
217             }
218            
219             elsif ( $OUTPUT_DEF{$k} eq 'regexp' )
220             {
221 0 0 0     0 croak "define_output: the value of '$k' must be a regexp or string"
222             if ref $v && reftype $v ne 'REGEXP';
223             }
224             }
225            
226             # Now make sure that each record has a 'type' attribute.
227            
228 2 50       7 croak "each record passed to define_output must include one attribute from the \
229             following list: 'include', 'output', 'set', 'select', 'filter'"
230             unless $type;
231            
232 2         4 return $type;
233             }
234              
235              
236             # _setup_output ( request )
237             #
238             # Determine the list of selection, processing and output rules for the
239             # specified query, based on the query's attributes. These attributes include:
240             #
241             # - the output map
242             # - the output format
243             # - the output vocabulary
244             # - the selected output keys
245             #
246             # Depending upon the attributes of the various output records, all, some or
247             # none of them may be relevant to a particular query.
248              
249             sub _setup_output {
250              
251 0     0   0 my ($ds, $request) = @_;
252            
253             # Extract the relevant attributes of the request
254            
255 0         0 my $path = $request->node_path;
256 0         0 my $format = $request->output_format;
257 0         0 my $vocab = $request->output_vocab;
258            
259 0 0 0     0 my $require_vocab; $require_vocab = 1 if $vocab and not $ds->{vocab}{$vocab}{use_field_names};
  0         0  
260            
261             # Add fields to the request object to hold the output configuration.
262            
263 0         0 $request->{select_list} = [];
264 0         0 $request->{select_hash} = {};
265 0         0 $request->{tables_hash} = {};
266 0         0 $request->{filter_hash} = {};
267 0         0 $request->{proc_list} = [];
268 0         0 $request->{field_list} = [];
269 0         0 $request->{block_keys} = {};
270 0         0 $request->{block_hash} = {};
271            
272             # Use the output and output_opt attributes of the request to determine
273             # which output blocks we will be using to express the request result.
274            
275             # We start with 'output', which specifies a list of blocks that are always
276             # included.
277            
278 0         0 my $output_list = $ds->node_attr($path, 'output');
279 0 0       0 my @output_list; @output_list = @$output_list if ref $output_list eq 'ARRAY';
  0         0  
280            
281 0         0 my @blocks;
282            
283 0         0 foreach my $block_name ( @output_list )
284             {
285 0 0       0 if ( ref $ds->{block}{$block_name} eq 'Web::DataService::Block' )
286             {
287 0         0 push @blocks, $block_name;
288             }
289            
290             else
291             {
292 0         0 $request->add_warning("Output block '$block_name' not found");
293             }
294             }
295            
296             # The special parameter 'show' is used to select optional output blocks.
297            
298 0         0 my @optional_keys = $request->special_value('show');
299            
300             # The attribute 'optional_output' specifies a map which maps the keys from the
301             # output_param value to block names. We go through the keys one by one
302             # and add each key and the name of the associated block to the relevant hash.
303            
304 0         0 my $optional_output = $ds->node_attr($path, 'optional_output');
305 0         0 my $output_map; $output_map = $ds->{set}{$optional_output} if defined $optional_output &&
306 0 0 0     0 ref $ds->{set}{$optional_output} eq 'Web::DataService::Set';
307            
308 0 0       0 if ( $output_map )
    0          
309             {
310 0         0 foreach my $key ( @optional_keys )
311             {
312 0 0       0 next unless defined $key;
313 0         0 my $block = $output_map->{value}{$key}{maps_to};
314 0         0 $request->{block_keys}{$key} = 1;
315 0         0 $request->{block_hash}{$key} = 1;
316            
317 0 0 0     0 if ( $block && ref $ds->{block}{$block} eq 'Web::DataService::Block' )
    0          
318             {
319 0         0 $request->{block_hash}{$block} = $key;
320 0         0 push @blocks, $block;
321             }
322            
323             elsif ( $block )
324             {
325 0         0 $request->add_warning("Output block '$block' not found");
326             }
327             }
328             }
329            
330             elsif ( $optional_output )
331             {
332 0         0 $request->add_warning("Output map '$optional_output' not found");
333             }
334            
335             # Now warn the user if no output blocks were specified for this request,
336             # because it means that no output will result.
337            
338 0 0       0 unless ( @blocks )
339             {
340 0         0 $request->add_warning("No output blocks were specified for this request.");
341 0         0 return;
342             }
343            
344             # Then scan through the list of blocks and check for include_list
345             # entries, and add the included blocks to the list as well. This
346             # allows us to know before the rest of the processing exactly which blocks
347             # are included.
348            
349 0         0 my %uniq_block;
350 0         0 my @include_scan = @blocks;
351 0         0 my $bound = 0;
352            
353             INCLUDE_BLOCK:
354 0         0 while ( my $block = shift @include_scan )
355             {
356             # Make sure that each block is checked only once, and add a bounds
357             # check to prevent a runaway loop.
358            
359 0 0       0 next if $uniq_block{$block}; $uniq_block{$block} = 1;
  0         0  
360 0 0       0 next if ++$bound > 999;
361            
362 0         0 my $include_list = $ds->{block}{$block}{include_list};
363 0 0       0 next unless ref $include_list eq 'ARRAY';
364            
365             INCLUDE_RECORD:
366 0         0 foreach my $r ( @$include_list )
367             {
368             # Evaluate dependency on the output section list
369            
370             next INCLUDE_RECORD if $r->{if_block}
371 0 0 0     0 and not check_set($r->{if_block}, $request->{block_hash});
372            
373             next INCLUDE_RECORD if $r->{not_block}
374 0 0 0     0 and check_set($r->{not_block}, $request->{block_hash});
375            
376             # Evaluate dependency on the output format
377            
378             next INCLUDE_RECORD if $r->{if_format}
379 0 0 0     0 and not check_value($r->{if_format}, $format);
380            
381             next INCLUDE_RECORD if $r->{not_format}
382 0 0 0     0 and check_value($r->{not_format}, $format);
383            
384             # Evaluate dependency on the vocabulary
385            
386             next INCLUDE_RECORD if $r->{if_vocab}
387 0 0 0     0 and not check_value($r->{if_vocab}, $vocab);
388            
389             next INCLUDE_RECORD if $r->{not_vocab}
390 0 0 0     0 and check_value($r->{not_vocab}, $vocab);
391            
392             # If the 'include' record specified a key, figure out its
393             # corresponding block if any.
394            
395 0         0 my ($include_key, $include_block);
396            
397 0 0       0 if ( ref $output_map->{value}{$r->{include}} )
398             {
399 0         0 $include_key = $r->{include};
400 0         0 $include_block = $output_map->{value}{$include_key}{maps_to};
401             }
402            
403             else
404             {
405 0         0 $include_block = $r->{include};
406             }
407            
408             # Modify the record so that we know what block to include in the
409             # loop below.
410            
411 0 0       0 $r->{include_block} = $include_block if $include_block;
412            
413             # Now add the specified key and block to the output hash, if they
414             # are defined.
415            
416 0 0       0 $request->{block_keys}{$include_key} = 1 if $include_key;
417 0 0       0 $request->{block_hash}{$include_block} = 1 if $include_block;
418 0 0       0 push @include_scan, $include_block if $include_block;
419             }
420             }
421            
422             # Now run through all of the blocks we have identified and collect up the
423             # various kinds of records they contain.
424            
425 0         0 %uniq_block = (); # $$$$
426            
427             BLOCK:
428 0         0 foreach my $block (@blocks)
429             {
430             # Add this block to the output configuration.
431            
432 0         0 $ds->add_output_block($request, \%uniq_block, $block);
433             }
434            
435 0         0 my $a = 1; # We can stop here when debugging
436             }
437              
438              
439             # add_output_block ( request, block_name )
440             #
441             # Add the specified block to the output configuration for the specified
442             # request.
443              
444             sub add_output_block {
445              
446 0     0 0 0 my ($ds, $request, $uniq_block, $block_name) = @_;
447            
448             # Make sure that each block is only processed once, even if it is
449             # listed more than once.
450            
451 0 0       0 return if $uniq_block->{$block_name}; $uniq_block->{$block_name} = 1;
  0         0  
452            
453             # Generate a warning if the specified block does not exist, but do
454             # not abort the request.
455            
456 0         0 my $block_list = $ds->{block}{$block_name}{output_list};
457            
458 0 0       0 unless ( ref $block_list eq 'ARRAY' )
459             {
460 0         0 warn "undefined output block '$block_name' for path '$request->{path}'\n";
461 0         0 $request->add_warning("undefined output block '$block_name'");
462 0         0 return;
463             }
464            
465             # Extract the relevant request attributes.
466            
467 0         0 my $class = ref $request;
468 0         0 my $format = $request->output_format;
469 0         0 my $vocab = $request->output_vocab;
470 0 0 0     0 my $require_vocab; $require_vocab = 1 if $vocab and not $ds->{vocab}{$vocab}{use_field_names};
  0         0  
471            
472             # Now go through the output list for this block and collect up
473             # all records that are selected for this query.
474            
475 0         0 my @records = @$block_list;
476            
477             RECORD:
478 0         0 while ( my $r = shift @records )
479             {
480             # Evaluate dependency on the output block list
481            
482             next RECORD if $r->{if_block}
483 0 0 0     0 and not check_set($r->{if_block}, $request->{block_hash});
484            
485             next RECORD if $r->{not_block}
486 0 0 0     0 and check_set($r->{not_block}, $request->{block_hash});
487            
488             # Evaluate dependency on the output format
489            
490             next RECORD if $r->{if_format}
491 0 0 0     0 and not check_value($r->{if_format}, $format);
492            
493             next RECORD if $r->{not_format}
494 0 0 0     0 and check_value($r->{not_format}, $format);
495            
496             # Evaluate dependency on the vocabulary
497            
498             next RECORD if $r->{if_vocab}
499 0 0 0     0 and not check_value($r->{if_vocab}, $vocab);
500            
501             next RECORD if $r->{not_vocab}
502 0 0 0     0 and check_value($r->{not_vocab}, $vocab);
503            
504             # If the record type is 'select', add to the selection list, the
505             # selection hash, and the tables hash.
506            
507 0 0 0     0 if ( $r->{select} )
    0          
    0          
    0          
    0          
508             {
509             croak "value of 'select' must be a string or array"
510 0 0 0     0 if ref $r->{select} && ref $r->{select} ne 'ARRAY';
511            
512 0         0 my @select = ref $r->{select} ? @{$r->{select}}
513 0 0       0 : split qr{\s*,\s*}, $r->{select};
514            
515 0         0 foreach my $s ( @select )
516             {
517 0 0       0 next if exists $request->{select_hash}{$s};
518 0         0 $request->{select_hash}{$s} = 1;
519 0         0 push @{$request->{select_list}}, $s;
  0         0  
520             }
521            
522 0 0       0 if ( $r->{tables} )
523             {
524             croak "value of 'tables' must be a string or array"
525 0 0 0     0 if ref $r->{tables} && ref $r->{tables} ne 'ARRAY';
526            
527 0         0 my @tables = ref $r->{tables} ? @{$r->{tables}}
528 0 0       0 : split qr{\s*,\s*}, $r->{tables};
529            
530 0         0 foreach my $t ( @tables )
531             {
532 0         0 $request->{tables_hash}{$t} = 1;
533             }
534             }
535            
536 0         0 foreach my $k ( keys %$r )
537             {
538             warn "ignored invalid key '$k' in 'select' record"
539 0 0       0 unless $SELECT_KEY{$k};
540             }
541             }
542            
543             # If the record type is 'filter', add to the filter hash.
544            
545             elsif ( defined $r->{filter} )
546             {
547 0         0 $request->{filter_hash}{$r->{filter}} = $r->{value};
548             }
549            
550             # If the record type is 'set' or 'check', add a record to the process list.
551            
552             elsif ( defined $r->{set} || defined $r->{check} )
553             {
554 0         0 my $proc = { set => $r->{set} };
555            
556 0         0 foreach my $key ( keys %$r )
557             {
558 0 0       0 if ( $PROC_KEY{$key} )
559             {
560 0         0 $proc->{$key} = $r->{$key};
561             }
562            
563             else
564             {
565 0         0 carp "Warning: unknown key '$key' in proc record\n";
566             }
567             }
568            
569 0         0 push @{$request->{proc_list}}, $proc;
  0         0  
570            
571             # If this is a 'check' rule, then complain if the values don't
572             # make sense. Also note that we will have to process the result
573             # set in its entirety if we need to compute the size.
574            
575 0 0       0 if ( defined $r->{check} )
576             {
577 0         0 $request->{process_before_count} = 1;
578            
579 0         0 my $check_value = $r->{check};
580            
581 0 0 0     0 if ( $check_value eq '*' || $check_value eq '' )
    0          
582             {
583             croak "the value of 'code' must be a code ref"
584 0 0       0 unless ref $r->{code} eq 'CODE';
585             }
586            
587             elsif ( defined $r->{lookup} )
588             {
589             croak "the value of 'lookup' must be a hash ref"
590 0 0       0 unless ref $r->{lookup} eq 'HASH';
591             }
592             }
593             }
594            
595             # If the record type is 'output', add a record to the field list.
596             # The attributes 'name' (the output name) and 'field' (the raw
597             # field name) are both set to the indicated name by default.
598            
599             elsif ( defined $r->{output} )
600             {
601 0 0       0 croak "the value of 'output' must be non-empty" unless $r->{output} ne '';
602            
603 0 0 0     0 next RECORD if $require_vocab and not exists $r->{"${vocab}_name"};
604            
605 0         0 my $field = { field => $r->{output}, name => $r->{output} };
606 0         0 my ($vs_value, $vs_name);
607            
608 0         0 foreach my $key ( keys %$r )
609             {
610 0 0       0 if ( $FIELD_KEY{$key} )
    0          
    0          
    0          
611             {
612 0 0 0     0 $field->{$key} = $r->{$key}
      0        
      0        
613             unless ($key eq 'value' && $vs_value) || ($key eq 'name' && $vs_name);
614             }
615            
616             elsif ( $key =~ qr{ ^ (\w+) _ (name|value) $ }x )
617             {
618 0 0 0     0 if ( $1 eq $vocab || $1 eq $format )
619             {
620 0         0 $field->{$2} = $r->{$key};
621 0 0       0 $vs_value = 1 if $2 eq 'value';
622 0 0       0 $vs_name = 1 if $2 eq 'name';
623             }
624             }
625            
626             elsif ( $key eq 'data_type' )
627             {
628 0         0 my $type_value = $r->{data_type};
629 0 0 0     0 croak "unknown value '$r->{data_type}' for data_type: must be one of 'int', 'pos', 'dec', 'str'"
      0        
      0        
630             unless lc $type_value eq 'int' || lc $type_value eq 'pos' ||
631             lc $type_value eq 'dec' || lc $type_value eq 'str';
632            
633 0         0 $field->{data_type} = $r->{data_type};
634 0         0 push @{$request->{proc_list}}, { check_field => $r->{output}, data_type => $r->{data_type} }
635 0 0       0 unless $r->{data_type} eq 'str';
636             }
637            
638             elsif ( $key ne 'output' )
639             {
640 0         0 warn "Warning: unknown key '$key' in output record\n";
641             }
642             }
643            
644 0         0 push @{$request->{field_list}}, $field;
  0         0  
645             }
646            
647             # If the record type is 'include', then add the specified records
648             # to the list immediately. If no 'include_block' was
649             # specified, that means that the specified key did not correspond
650             # to any block. So we can ignore it in that case.
651            
652             elsif ( defined $r->{include_block} )
653             {
654             # If we have already processed this block, then skip it. A
655             # block can only be included once per request. If we haven't
656             # processed it yet, mark it so that it will be skipped if it
657             # comes up again.
658            
659 0         0 my $include_block = $r->{include_block};
660 0 0       0 next RECORD if $uniq_block->{$include_block};
661 0         0 $uniq_block->{$include_block} = 1;
662            
663             # Get the list of block records, or add a warning if no block
664             # was defined under that name.
665            
666 0         0 my $add_list = $ds->{block}{$include_block}{output_list};
667            
668 0 0       0 unless ( ref $add_list eq 'ARRAY' )
669             {
670 0         0 warn "undefined output block '$include_block' for path '$request->{path}'\n";
671 0         0 $request->add_warning("undefined output block '$include_block'");
672 0         0 next RECORD;
673             }
674            
675             # Now add the included block's records to the front of the
676             # record list.
677            
678 0         0 unshift @records, @$add_list;
679             }
680             }
681            
682 0         0 my $a = 1; # we can stop here when debugging
683             }
684              
685              
686             # get_output_map ( name )
687             #
688             # If the specified name is the name of an output map, return a reference to
689             # the map. Otherwise, return undefined.
690              
691             sub get_output_map {
692            
693 0     0 0 0 my ($ds, $output_name) = @_;
694            
695 0 0       0 if ( ref $ds->{set}{$output_name} eq 'Web::DataService::Set' )
696             {
697 0         0 return $ds->{set}{$output_name};
698             }
699            
700 0         0 return;
701             }
702              
703              
704             # get_output_block ( name )
705             #
706             # If the specified name is the name of an output block, return a reference to
707             # the block. Otherwise, return empty.
708              
709             sub get_output_block {
710              
711 0     0 0 0 my ($ds, $output_name) = @_;
712            
713 0 0       0 if ( ref $ds->{block}{$output_name} eq 'Web::DataService::Block' )
714             {
715 0         0 return $ds->{block}{$output_name};
716             }
717            
718 0         0 return;
719             }
720              
721              
722             # get_output_keys ( request, map )
723             #
724             # Figure out which output keys have been selected for the specified request,
725             # using the specified output map.
726              
727             sub get_output_keys {
728            
729 0     0 0 0 my ($ds, $request, $output_map) = @_;
730            
731 0         0 my $path = $request->{path};
732            
733             # Return empty unless we have a map.
734            
735 0 0       0 return unless ref $output_map eq 'Web::DataService::Set';
736            
737             # Start with the fixed blocks.
738            
739 0 0       0 my @keys; @keys = @{$output_map->{fixed}} if ref $output_map->{fixed} eq 'ARRAY';
  0         0  
  0         0  
740            
741             # Then add the optional blocks.
742            
743 0         0 my $output_param = $ds->{node_attrs}{$path}{output_param}; # re-do
744             # with ->node_attrs
745            
746 0         0 push @keys, @{$request->{params}{$output_param}}
747 0 0 0     0 if defined $output_param and ref $request->{params}{$output_param} eq 'ARRAY';
748            
749 0         0 return @keys;
750             }
751              
752              
753             # configure_block ( request, block_name )
754             #
755             # Given a block name, determine the list of output fields and proc fields
756             # (if any) that are defined for it. This is used primarily to configure
757             # blocks referred to via 'sub_record' attributes.
758             #
759             # These lists are stored under the keys 'block_proc_list' and
760             # 'block_field_list' in the request record. If these have already been filled
761             # in for this block, do nothing.
762              
763             sub configure_block {
764              
765 0     0 0 0 my ($ds, $request, $block_name) = @_;
766            
767             # Return immediately if the relevant lists have already been computed
768             # and cached (even if they are empty).
769            
770 0 0       0 return 1 if exists $request->{block_field_list}{$block_name};
771            
772             # Otherwise, we need to compute both lists. Start by determining the
773             # relevant attributes of the request and looking up the output list
774             # for this block.
775            
776 0         0 my $format = $request->output_format;
777 0         0 my $vocab = $request->output_vocab;
778 0 0 0     0 my $require_vocab; $require_vocab = 1 if $vocab and not $ds->{vocab}{$vocab}{use_field_names};
  0         0  
779            
780 0         0 my $block_list = $ds->{block}{$block_name}{output_list};
781            
782             # If no list is available, indicate this to the request object and return
783             # false. Whichever routine called us will be responsible for generating an
784             # error or warning if appropriate.
785            
786 0 0       0 unless ( ref $block_list eq 'ARRAY' )
787             {
788 0         0 $request->{block_field_list}{$block_name} = undef;
789 0         0 $request->{block_proc_list}{$block_name} = undef;
790 0         0 return;
791             }
792            
793             # Go through each record in the list, throwing out the ones that don't
794             # apply and assigning the ones that do.
795            
796 0         0 my (@field_list, @proc_list);
797            
798             RECORD:
799 0         0 foreach my $r ( @$block_list )
800             {
801             # Evaluate dependency on the output block list
802            
803             next RECORD if $r->{if_block}
804 0 0 0     0 and not check_set($r->{if_block}, $request->{block_set});
805            
806             next RECORD if $r->{not_block}
807 0 0 0     0 and check_set($r->{not_block}, $request->{block_set});
808            
809             # Evaluate dependency on the output format
810            
811             next RECORD if $r->{if_format}
812 0 0 0     0 and not check_value($r->{if_format}, $format);
813            
814             next RECORD if $r->{not_format}
815 0 0 0     0 and check_value($r->{not_format}, $format);
816            
817             # Evaluate dependency on the vocabulary
818            
819             next RECORD if $r->{if_vocab}
820 0 0 0     0 and not check_value($r->{if_vocab}, $vocab);
821            
822             next RECORD if $r->{not_vocab}
823 0 0 0     0 and check_value($r->{not_vocab}, $vocab);
824            
825             # If the record type is 'output', add a record to the field list.
826             # The attributes 'name' (the output name) and 'field' (the raw
827             # field name) are both set to the indicated name by default.
828            
829 0 0       0 if ( defined $r->{output} )
    0          
830             {
831 0 0 0     0 next RECORD if $require_vocab and not exists $r->{"${vocab}_name"};
832            
833 0         0 my $output = { field => $r->{output}, name => $r->{output} };
834            
835 0         0 foreach my $key ( keys %$r )
836             {
837 0 0       0 if ( $FIELD_KEY{$key} )
    0          
    0          
    0          
838             {
839 0         0 $output->{$key} = $r->{$key};
840             }
841            
842             elsif ( $key =~ qr{ ^ (\w+) _ (name|value) $ }x )
843             {
844 0 0       0 $output->{$2} = $r->{$key} if $vocab eq $1;
845             }
846            
847             elsif ( $key eq 'data_type' )
848             {
849 0         0 my $type_value = $r->{data_type};
850 0 0 0     0 croak "unknown value '$r->{data_type}' for data_type: must be one of 'int', 'pos', 'dec', 'str'"
      0        
      0        
851             unless lc $type_value eq 'int' || lc $type_value eq 'pos' ||
852             lc $type_value eq 'dec' || lc $type_value eq 'str';
853            
854 0         0 $output->{data_type} = $r->{data_type};
855 0         0 push @{$request->{proc_list}}, { check_field => $r->{output}, data_type => $r->{data_type} }
856 0 0       0 unless $r->{data_type} eq 'str';
857             }
858            
859             elsif ( $key ne 'output' )
860             {
861 0         0 warn "Warning: unknown key '$key' in output record\n";
862             }
863             }
864            
865 0         0 push @field_list, $output;
866             }
867            
868             # If the record type is 'set', add a record to the proc list.
869            
870             elsif ( defined $r->{set} )
871             {
872 0         0 my $proc = { set => $r->{set} };
873            
874 0         0 foreach my $key ( keys %$r )
875             {
876 0 0       0 if ( $PROC_KEY{$key} )
877             {
878 0         0 $proc->{$key} = $r->{$key};
879             }
880            
881             else
882             {
883 0         0 carp "Warning: unknown key '$key' in proc record\n";
884             }
885             }
886            
887 0         0 push @proc_list, $proc;
888             }
889            
890             # All other record types are ignored.
891             }
892            
893             # Now cache the results.
894            
895 0         0 $request->{block_field_list}{$block_name} = \@field_list;
896 0         0 $request->{block_proc_list}{$block_name} = \@proc_list;
897            
898 0         0 return 1;
899             }
900              
901              
902             # check_value ( list, value )
903             #
904             # Return true if $list is equal to $value, or if it is a list and one if its
905             # items is equal to $value.
906              
907             sub check_value {
908            
909 0     0 0 0 my ($list, $value) = @_;
910            
911 0 0       0 return 1 if $list eq $value;
912            
913 0 0       0 if ( ref $list eq 'ARRAY' )
914             {
915 0         0 foreach my $item (@$list)
916             {
917 0 0       0 return 1 if $item eq $value;
918             }
919             }
920            
921 0         0 return;
922             }
923              
924              
925             # check_set ( list, set )
926             #
927             # The parameter $set must be a hashref. Return true if $list is one of the
928             # keys of $set, or if it $list is a list and one of its items is a key in
929             # $set. A key only counts if it has a true value.
930              
931             sub check_set {
932            
933 0     0 0 0 my ($list, $set) = @_;
934            
935 0 0       0 return unless ref $set eq 'HASH';
936            
937 0 0       0 return 1 if $set->{$list};
938            
939 0 0       0 if ( ref $list eq 'ARRAY' )
940             {
941 0         0 foreach my $item (@$list)
942             {
943 0 0       0 return 1 if $set->{$item};
944             }
945             }
946            
947 0         0 return;
948             }
949              
950              
951             # add_doc ( node, item )
952             #
953             # Add the specified item to the documentation list for the specified node.
954             # The item can be either a string or a record (hashref).
955              
956             sub add_doc {
957              
958 8     8 0 15 my ($ds, $node, $item) = @_;
959            
960             # If the item is a record, close any currently pending documentation and
961             # start a new "pending" list. We need to do this because subsequent items
962             # may document the record we were just called with.
963            
964 8 100       46 if ( ref $item )
    50          
965             {
966 4 50       16 croak "cannot add non-hash object to documentation"
967             unless reftype $item eq 'HASH';
968            
969 4         11 $ds->process_doc($node);
970 4         8 push @{$node->{doc_pending}}, $item;
  4         20  
971             }
972            
973             # If this is a string starting with one of the special characters, then
974             # handle it properly.
975            
976             elsif ( $item =~ qr{ ^ ([!^?] | >>?) (.*) }xs )
977             {
978             # If >>, then close the active documentation section (if any) and
979             # start a new one that is not tied to any rule. This will generate an
980             # ordinary paragraph starting with the remainder of the line.
981            
982 0 0       0 if ( $1 eq '>>' )
    0          
    0          
983             {
984 0         0 $ds->process_doc($node);
985 0 0       0 push @{$node->{doc_pending}}, $2 if $2 ne '';
  0         0  
986             }
987            
988             # If >, then add to the current documentation a blank line
989             # (which will cause a new paragraph) followed by the remainder
990             # of this line.
991            
992             elsif ( $1 eq '>' )
993             {
994 0         0 push @{$node->{doc_pending}}, "\n$2";
  0         0  
995             }
996            
997             # If !, then discard all pending documentation and mark the node as
998             # 'undocumented'. This will cause it to be elided from the documentation.
999            
1000             elsif ( $1 eq '!' )
1001             {
1002 0         0 $ds->process_doc($node, 'undocumented');
1003             }
1004            
1005             # If ?, then add the remainder of the line to the documentation.
1006             # The ! prevents the next character from being interpreted specially.
1007            
1008             else
1009             {
1010 0         0 push @{$node->{doc_pending}}, $2;
  0         0  
1011             }
1012             }
1013            
1014             # Otherwise, just add this string to the "pending" list.
1015            
1016             else
1017             {
1018 4         8 push @{$node->{doc_pending}}, $item;
  4         10  
1019             }
1020             }
1021              
1022              
1023             # process_doc ( node, disposition )
1024             #
1025             # Process all pending documentation items.
1026              
1027             sub process_doc {
1028              
1029 6     6 0 13 my ($ds, $node, $disposition) = @_;
1030            
1031             # Return immediately unless we have something pending.
1032            
1033 6 100 66     21 return unless ref $node->{doc_pending} eq 'ARRAY' && @{$node->{doc_pending}};
  4         20  
1034            
1035             # If the "pending" list starts with an item record, take that off first.
1036             # Everything else on the list should be a string.
1037            
1038 4         11 my $primary_item = shift @{$node->{doc_pending}};
  4         9  
1039 4 50       10 return unless ref $primary_item;
1040            
1041             # Discard all pending documentation if the primary item is disabled or
1042             # marked with a '!'. In the latter case, note this in the item record.
1043            
1044 4   50     16 $disposition //= '';
1045            
1046 4 50 33     31 if ( $primary_item->{disabled} or $primary_item->{undocumented} or
      33        
1047             $disposition eq 'undocumented' )
1048             {
1049 0         0 @{$node->{doc_pending}} = ();
  0         0  
1050 0 0       0 $primary_item->{undocumented} = 1 if $disposition eq 'undocumented';
1051 0         0 return;
1052             }
1053            
1054             # Put the rest of the documentation items together into a single
1055             # string, which may contain a series of Pod paragraphs.
1056            
1057 4         8 my $body = '';
1058 4         7 my $last_pod;
1059             my $this_pod;
1060            
1061 4         7 while (my $line = shift @{$node->{doc_pending}})
  8         19  
1062             {
1063             # If this line starts with =, then it needs extra spacing.
1064            
1065 4         35 my $this_pod = $line =~ qr{ ^ = }x;
1066            
1067             # If $body already has something in it, add a newline first. Add
1068             # two if this line starts with =, or if the previously added line
1069             # did, so that we get a new paragraph.
1070            
1071 4 50       15 if ( $body ne '' )
1072             {
1073 0 0 0     0 $body .= "\n" if $last_pod || $this_pod;
1074 0         0 $body .= "\n";
1075             }
1076            
1077 4         8 $body .= $line;
1078 4         8 $last_pod = $this_pod;
1079             }
1080            
1081             # Then add the documentation to the node's documentation list. If there
1082             # is no primary item, add the body as an ordinary paragraph.
1083            
1084 4 50       11 unless ( defined $primary_item )
1085             {
1086 0         0 push @{$node->{doc_list}}, clean_doc($body);
  0         0  
1087             }
1088            
1089             # Otherwise, attach the body to the primary item and add it to the list.
1090            
1091             else
1092             {
1093 4         11 $primary_item->{doc_string} = clean_doc($body, 1);
1094 4         12 push @{$node->{doc_list}}, $primary_item;
  4         16  
1095             }
1096             }
1097              
1098              
1099             # clean_doc ( )
1100             #
1101             # Make sure that the indicated string is valid POD. In particular, if there
1102             # are any unclosed =over sections, close them at the end. Throw an exception
1103             # if we find an =item before the first =over or a =head inside an =over.
1104              
1105             sub clean_doc {
1106              
1107 4     4 0 8 my ($docstring, $item_body) = @_;
1108            
1109 4         6 my $list_level = 0;
1110            
1111 4         12 while ( $docstring =~ / ^ (=[a-z]+) /gmx )
1112             {
1113 0 0       0 if ( $1 eq '=over' )
    0          
    0          
    0          
1114             {
1115 0         0 $list_level++;
1116             }
1117            
1118             elsif ( $1 eq '=back' )
1119             {
1120 0         0 $list_level--;
1121 0 0       0 croak "invalid POD string: =back does not match any =over" if $list_level < 0;
1122             }
1123            
1124             elsif ( $1 eq '=item' )
1125             {
1126 0 0       0 croak "invalid POD string: =item outside of =over" if $list_level == 0;
1127             }
1128            
1129             elsif ( $1 eq '=head' )
1130             {
1131 0 0 0     0 croak "invalid POD string: =head inside =over" if $list_level > 0 || $item_body;
1132             }
1133             }
1134            
1135 4         10 $docstring .= "\n\n=back" x $list_level;
1136            
1137 4         11 return $docstring;
1138             }
1139              
1140              
1141             # document_node ( node, state )
1142             #
1143             # Return a documentation string for the given node, in Pod format. This will
1144             # consist of a main item list that may start and stop, possibly with ordinary
1145             # Pod paragraphs in between list chunks. If this node contains any 'include'
1146             # records, the lists for those nodes will be recursively interpolated into the
1147             # main list. Sublists can only occur if they are explicitly included in the
1148             # documentation strings for individual node records.
1149             #
1150             # If the $state parameter is given, it must be a hashref containing any of the
1151             # following keys:
1152             #
1153             # namespace A hash ref in which included nodes may be looked up by name.
1154             # If this is not given, then 'include' records are ignored.
1155             #
1156             # items_only If true, then ordinary paragraphs will be ignored and a single
1157             # uninterrupted item list will be generated.
1158             #
1159              
1160             sub document_node {
1161            
1162 0     0 0   my ($ds, $node, $state) = @_;
1163            
1164             # Return the empty string unless documentation has been added to this
1165             # node.
1166            
1167 0 0 0       return '' unless ref $node && ref $node->{doc_list} eq 'ARRAY';
1168            
1169             # Make sure we have a state record, if we were not passed one.
1170            
1171 0   0       $state ||= {};
1172            
1173             # Make sure that we process each node only once, if it should happen
1174             # to be included multiple times. Also keep track of our recursion level.
1175            
1176 0 0         return if $state->{processed}{$node->{name}};
1177            
1178 0           $state->{processed}{$node->{name}} = 1;
1179 0           $state->{level}++;
1180            
1181             # Go through the list of documentation items, treating each one as a Pod
1182             # paragraph. That means that they will be separated from each other by a
1183             # blank line. List control paragraphs "=over" and "=back" will be added
1184             # as necessary to start and stop the main item list.
1185            
1186 0           my $doc = '';
1187            
1188             ITEM:
1189 0           foreach my $item ( @{$node->{doc_list}} )
  0            
1190             {
1191             # A string is added as an ordinary paragraph. The main list is closed
1192             # if it is open. But the item is skipped if we were given the
1193             # 'items_only' flag.
1194            
1195 0 0         unless ( ref $item )
    0          
1196             {
1197 0 0         next ITEM if $state->{items_only};
1198            
1199 0 0         if ( $state->{in_list} )
1200             {
1201 0 0         $doc .= "\n\n" if $doc ne '';
1202 0           $doc .= "=back";
1203 0           $state->{in_list} = 0;
1204             }
1205            
1206 0 0 0       $doc .= "\n\n" if $doc ne '' && $item ne '';
1207 0           $doc .= $item;
1208             }
1209            
1210             # An 'include' record inserts the documentation for the specified
1211             # node. This does not necessarily end the list, only if the include
1212             # record itself has a documentation string. Skip the inclusion if no
1213             # hashref was provided for looking up item names.
1214            
1215 0           elsif ( defined $item->{include} )
1216             {
1217 0 0 0       next ITEM unless ref $state->{namespace} && reftype $state->{namespace} eq 'HASH';
1218            
1219 0 0 0       if ( defined $item->{doc_string} and $item->{doc_string} ne '' and not $state->{items_only} )
      0        
1220             {
1221 0 0         if ( $state->{in_list} )
1222             {
1223 0 0         $doc .= "\n\n" if $doc ne '';
1224 0           $doc .= "=back";
1225 0           $state->{in_list} = 0;
1226             }
1227            
1228 0 0         $doc .= "\n\n" if $doc ne '';
1229 0           $doc .= $item->{doc_string};
1230             }
1231            
1232 0           my $included_node = $state->{namespace}{$item->{include}};
1233            
1234 0 0 0       next unless ref $included_node && reftype $included_node eq 'HASH';
1235            
1236 0           my $subdoc = $ds->document_node($included_node, $state);
1237            
1238 0 0 0       $doc .= "\n\n" if $doc ne '' && $subdoc ne '';
1239 0           $doc .= $subdoc;
1240             }
1241            
1242             # Any other record is added as a list item. Try to figure out the
1243             # item name as best we can.
1244            
1245             else
1246             {
1247             my $name = ref $node eq 'Web::DataService::Set' ? $item->{value}
1248             : defined $item->{name} ? $item->{name}
1249 0 0         : '';
    0          
1250            
1251 0   0       $name ||= '';
1252            
1253 0 0         unless ( $state->{in_list} )
1254             {
1255 0 0         $doc .= "\n\n" if $doc ne '';
1256 0           $doc .= "=over";
1257 0           $state->{in_list} = 1;
1258             }
1259            
1260 0           $doc .= "\n\n=item $name";
1261 0 0 0       $doc .= "\n\n$item->{doc_string}" if defined $item->{doc_string} && $item->{doc_string} ne '';
1262             }
1263             }
1264            
1265             # If we get to the end of the top-level ruleset and we are still in a
1266             # list, close it. Also make sure that our resulting documentation string
1267             # ends with a newline.
1268            
1269 0 0         if ( --$state->{level} == 0 )
1270             {
1271 0 0         $doc .= "\n\n=back" if $state->{in_list};
1272 0           $state->{in_list} = 0;
1273 0           $doc .= "\n";
1274             }
1275            
1276 0           return $doc;
1277             }
1278              
1279              
1280             # document_response ( )
1281             #
1282             # Generate documentation in Pod format describing the available output fields
1283             # for the specified URL path.
1284              
1285             sub document_response {
1286            
1287 0     0 0   my ($ds, $path) = @_;
1288            
1289 0           my @blocks;
1290             my @labels;
1291            
1292             # First collect up a list of all of the fixed (non-optional) blocks.
1293             # Block names that do not correspond to any defined block are ignored,
1294             # with a warning.
1295            
1296 0   0       my $output_list = $ds->node_attr($path, 'output') // [ ];
1297 0   0       my $fixed_label = $ds->node_attr($path, 'output_label') // 'basic';
1298            
1299 0           foreach my $block_name ( @$output_list )
1300             {
1301 0 0         if ( ref $ds->{block}{$block_name} eq 'Web::DataService::Block' )
    0          
1302             {
1303 0           push @blocks, $block_name;
1304 0           push @labels, $fixed_label;
1305             }
1306            
1307             elsif ( $ds->debug )
1308             {
1309             warn "WARNING: block '$block_name' not found"
1310 0 0 0       unless $Web::DataService::QUIET || $ENV{WDS_QUIET};
1311             }
1312             }
1313            
1314             # Then add all of the optional blocks, if an output_opt map was
1315             # specified.
1316            
1317 0           my $optional_output = $ds->node_attr($path, 'optional_output');
1318 0           my $reverse_map;
1319            
1320 0 0 0       if ( $optional_output && ref $ds->{set}{$optional_output} eq 'Web::DataService::Set' )
    0 0        
1321             {
1322 0           my $output_map = $ds->{set}{$optional_output};
1323 0 0         my @keys; @keys = @{$output_map->{value_list}} if ref $output_map->{value_list} eq 'ARRAY';
  0            
  0            
1324            
1325             VALUE:
1326 0           foreach my $label ( @keys )
1327             {
1328 0           my $block_name = $output_map->{value}{$label}{maps_to};
1329 0 0         next VALUE unless defined $block_name;
1330             next VALUE if $output_map->{value}{$label}{disabled} ||
1331 0 0 0       $output_map->{value}{$label}{undocumented};
1332            
1333 0           $reverse_map->{$block_name} = $label;
1334            
1335 0 0         if ( ref $ds->{block}{$block_name} eq 'Web::DataService::Block' )
1336             {
1337 0           push @blocks, $block_name;
1338 0           push @labels, $label;
1339             }
1340             }
1341             }
1342            
1343             elsif ( $optional_output && $ds->debug )
1344             {
1345             warn "WARNING: output map '$optional_output' not found"
1346 0 0 0       unless $Web::DataService::QUIET || $ENV{WDS_QUIET};
1347             }
1348            
1349             # If there are no output blocks specified for this path, return an empty
1350             # string.
1351            
1352 0 0         return '' unless @blocks;
1353            
1354             # Otherwise, determine the set of vocabularies that are allowed for this
1355             # path. If none are specifically selected for this path, then all of the
1356             # vocabularies defined for this data service are allowed.
1357            
1358 0   0       my $vocabularies; $vocabularies = $ds->node_attr($path, 'allow_vocab') || $ds->{vocab};
  0            
1359            
1360 0 0 0       unless ( ref $vocabularies eq 'HASH' && keys %$vocabularies )
1361             {
1362 0 0         warn "No output vocabularies were selected for path '$path'" if $ds->debug;
1363 0           return '';
1364             }
1365            
1366             my @vocab_list = grep { $vocabularies->{$_} &&
1367             ref $ds->{vocab}{$_} &&
1368 0 0 0       ! $ds->{vocab}{$_}{disabled} } @{$ds->{vocab_list}};
  0            
  0            
1369            
1370 0 0         unless ( @vocab_list )
1371             {
1372 0 0         warn "No output vocabularies were selected for path '$path'" if $ds->debug;
1373 0           return "";
1374             }
1375            
1376             # Now generate the header for the documentation, in Pod format. We
1377             # include the special "=for wds_table_header" line to give PodParser.pm the
1378             # information it needs to generate an HTML table.
1379            
1380 0           my $doc_string = '';
1381 0           my $field_count = scalar(@vocab_list);
1382 0           my $field_string = join ' / ', @vocab_list;
1383            
1384 0 0         if ( $field_count > 1 )
1385             {
1386 0           $doc_string .= "=for wds_table_header Field name*/$field_count | Block | Description\n\n";
1387 0           $doc_string .= "=over 4\n\n";
1388 0           $doc_string .= "=item $field_string\n\n";
1389             }
1390            
1391             else
1392             {
1393 0           $doc_string .= "=for wds_table_header Field name* | Block | Description\n\n";
1394 0           $doc_string .= "=over 4\n\n";
1395             }
1396            
1397             # Run through each block one at a time, documenting all of the fields in
1398             # the corresponding field list.
1399            
1400 0           my %uniq_block;
1401            
1402 0           foreach my $i (0..$#blocks)
1403             {
1404 0           my $block_name = $blocks[$i];
1405 0           my $block_label = $labels[$i];
1406            
1407             # Make sure to only process each block once, even if it is listed more
1408             # than once.
1409            
1410 0 0         next if $uniq_block{$block_name}; $uniq_block{$block_name} = 1;
  0            
1411            
1412 0           my $output_list = $ds->{block}{$block_name}{output_list};
1413 0 0         next unless ref $output_list eq 'ARRAY';
1414            
1415 0           foreach my $r (@$output_list)
1416             {
1417 0 0         next unless defined $r->{output};
1418             $doc_string .= $ds->document_field($block_label, \@vocab_list, $r, $reverse_map)
1419 0 0         unless $r->{undocumented};
1420             }
1421             }
1422            
1423 0           $doc_string .= "\n=back\n\n";
1424            
1425 0           return $doc_string;
1426             }
1427              
1428              
1429             sub document_summary {
1430              
1431 0     0 0   my ($ds, $path) = @_;
1432            
1433             # Return the empty string unless a summary block was defined for this path.
1434            
1435 0           my $summary_block = $ds->node_attr($path, 'summary');
1436 0 0         return '' unless $summary_block;
1437            
1438             # Otherwise, determine the set of vocabularies that are allowed for this
1439             # path. If none are specifically selected for this path, then all of the
1440             # vocabularies defined for this data service are allowed.
1441            
1442 0   0       my $vocabularies; $vocabularies = $ds->node_attr($path, 'allow_vocab') || $ds->{vocab};
  0            
1443            
1444 0 0 0       unless ( ref $vocabularies eq 'HASH' && keys %$vocabularies )
1445             {
1446 0           return '';
1447             }
1448            
1449             my @vocab_list = grep { $vocabularies->{$_} &&
1450             ref $ds->{vocab}{$_} &&
1451 0 0 0       ! $ds->{vocab}{$_}{disabled} } @{$ds->{vocab_list}};
  0            
  0            
1452            
1453 0 0         unless ( @vocab_list )
1454             {
1455 0           return "";
1456             }
1457            
1458             # Now generate the header for the documentation, in Pod format. We
1459             # include the special "=for wds_table_header" line to give PodParser.pm the
1460             # information it needs to generate an HTML table.
1461            
1462 0           my $doc_string = '';
1463 0           my $field_count = scalar(@vocab_list);
1464 0           my $field_string = join ' / ', @vocab_list;
1465            
1466 0 0         if ( $field_count > 1 )
1467             {
1468 0           $doc_string .= "=for wds_table_header Field name*/$field_count | Block | Description\n\n";
1469 0           $doc_string .= "=over 4\n\n";
1470 0           $doc_string .= "=item $field_string\n\n";
1471             }
1472            
1473             else
1474             {
1475 0           $doc_string .= "=for wds_table_header Field name* | Block | Description\n\n";
1476 0           $doc_string .= "=over 4\n\n";
1477             }
1478            
1479             # Now determine the summary output list.
1480            
1481 0           my $output_list = $ds->{block}{$summary_block}{output_list};
1482 0 0         return '' unless ref $output_list eq 'ARRAY';
1483            
1484 0           foreach my $r (@$output_list)
1485             {
1486 0 0         next unless defined $r->{output};
1487             $doc_string .= $ds->document_field('summary', \@vocab_list, $r, {})
1488 0 0         unless $r->{undocumented};
1489             }
1490            
1491 0           $doc_string .= "\n=back\n\n";
1492            
1493 0           return $doc_string;
1494             }
1495              
1496              
1497             sub document_field {
1498            
1499 0     0 0   my ($ds, $block_key, $vocab_list, $r, $rev_map) = @_;
1500            
1501 0           my @names;
1502            
1503 0           foreach my $v ( @$vocab_list )
1504             {
1505             my $n = defined $r->{"${v}_name"} ? $r->{"${v}_name"}
1506             : defined $r->{name} ? $r->{name}
1507             : $ds->{vocab}{$v}{use_field_names} ? $r->{output}
1508 0 0         : '';
    0          
    0          
1509            
1510 0   0       $n ||= 'I';
1511            
1512 0           push @names, $n
1513             }
1514            
1515 0           my $names = join ' / ', @names;
1516            
1517 0   0       my $descrip = $r->{doc_string} || "";
1518            
1519 0 0         if ( defined $r->{if_block} )
1520             {
1521 0 0         if ( ref $r->{if_block} eq 'ARRAY' )
1522             {
1523 0   0       $block_key = join(', ', map { $rev_map->{$_} // $_ } @{$r->{if_block}});
  0            
  0            
1524             }
1525             else
1526             {
1527 0   0       $block_key = $rev_map->{$r->{if_block}} // $r->{if_block};
1528             }
1529             }
1530            
1531 0           my $line = "\n=item $names ( $block_key )\n\n$descrip\n";
1532            
1533 0           return $line;
1534             }
1535              
1536              
1537             # process_record ( request, record, steps )
1538             #
1539             # Execute any per-record processing steps that have been defined for this
1540             # record. Return true if the record is to be included in the result, false
1541             # otherwise.
1542              
1543             sub process_record {
1544            
1545 0     0 0   my ($ds, $request, $record, $steps) = @_;
1546            
1547             # If there are no processing steps to do, return immediately.
1548            
1549 0 0 0       return 1 unless ref $steps eq 'ARRAY' and @$steps;
1550            
1551             # Otherwise go through the steps one by one.
1552            
1553 0           foreach my $p ( @$steps )
1554             {
1555             # Skip this processing step based on a conditional field value, if one
1556             # is defined.
1557            
1558 0 0         if ( my $cond_field = $p->{if_field} )
    0          
1559             {
1560 0 0         next unless defined $record->{$cond_field};
1561 0 0 0       next if ref $record->{$cond_field} eq 'ARRAY' && @{$record->{$cond_field}} == 0;
  0            
1562             }
1563            
1564             elsif ( $cond_field = $p->{not_field} )
1565             {
1566 0 0 0       next if defined $record->{$cond_field} && ref $record->{$cond_field} ne 'ARRAY';
1567 0 0 0       next if ref $record->{$cond_field} eq 'ARRAY' && @{$record->{$cond_field}} > 0;
  0            
1568             }
1569            
1570             # If this step is a 'check_field' step, then do the check.
1571            
1572 0 0         if ( defined $p->{check_field} )
    0          
1573             {
1574 0           $ds->check_field_type($record, $p->{check_field}, $p->{data_type}, $p->{subst});
1575 0           next;
1576             }
1577            
1578             # If this step is a 'check' step (i.e. check the entire record) then
1579             # do the check. If it fails, we return false.
1580            
1581             elsif ( defined $p->{check} )
1582             {
1583 0           my $check_value = $p->{check};
1584            
1585             # If the value is '*' or the empty string, then we must have a
1586             # code reference to call.
1587            
1588 0 0 0       if ( $check_value eq '' || $check_value eq '*' )
    0 0        
    0          
    0          
1589             {
1590 0           return $p->{code}($request, $record);
1591             }
1592            
1593             # Otherwise, if the value is '1' or '0' then return that. The
1594             # former will cause the record to be included in the result, the
1595             # latter will cause it to be skipped. This is mainly useful in
1596             # conjunction with 'if_field' or 'not_field'.
1597            
1598             elsif ( $check_value eq '1' || $check_value eq '0' )
1599             {
1600 0           return $check_value;
1601             }
1602            
1603             # Otherwise, we assume that we have been given a field name and
1604             # either call a code reference or do a hash-table lookup.
1605            
1606             elsif ( defined $p->{code} )
1607             {
1608 0           my $value = $record->{$check_value};
1609 0           return $p->{code}($request, $value);
1610             }
1611            
1612             elsif ( defined $p->{lookup} )
1613             {
1614 0           my $value = $record->{$check_value};
1615 0   0       return $p->{lookup}{$value} // $p->{default};
1616             }
1617            
1618             # Otherwise, we just return the value of the specified field. The
1619             # record will be included if this value is true, and skipped if
1620             # false.
1621            
1622             else
1623             {
1624 0           return $record->{$check_value};
1625             }
1626            
1627 0           next;
1628             }
1629            
1630             # If we get here, the current rule must be a 'set'. Figure out which
1631             # field (if any) we are affecting. A value of '*' means to use the
1632             # entire record (only relevant with 'code').
1633            
1634 0           my $set_field = $p->{set};
1635            
1636             # Figure out which field (if any) we are looking at. Skip this
1637             # processing step if the source field is empty, unless the attribute
1638             # 'always' is set.
1639            
1640 0   0       my $source_field = $p->{from} || $p->{from_each} || $p->{set};
1641            
1642             # Skip any processing step if the record does not have a non-empty
1643             # value in the corresponding field (unless the 'always' attribute is
1644             # set).
1645            
1646 0 0 0       if ( $source_field && $source_field ne '*' && ! $p->{always} )
      0        
1647             {
1648 0 0         next unless defined $record->{$source_field};
1649 0 0 0       next if ref $record->{$source_field} eq 'ARRAY' && @{$record->{$source_field}} == 0;
  0            
1650             }
1651            
1652             # Now generate a list of result values, according to the attributes of this
1653             # processing step.
1654            
1655 0           my @result;
1656            
1657             # If we have a 'code' attribute, then call it.
1658            
1659 0 0         if ( ref $p->{code} eq 'CODE' )
    0          
    0          
    0          
1660             {
1661 0 0         if ( $source_field eq '*' )
    0          
    0          
1662             {
1663 0           @result = $p->{code}($request, $record);
1664             }
1665            
1666             elsif ( $p->{from_each} )
1667             {
1668 0           @result = map { $p->{code}($request, $_) }
1669             (ref $record->{$source_field} eq 'ARRAY' ?
1670 0 0         @{$record->{$source_field}} : $record->{$source_field});
  0            
1671             }
1672            
1673             elsif ( $p->{from} )
1674             {
1675 0           @result = $p->{code}($request, $record->{$source_field});
1676             }
1677            
1678             else
1679             {
1680 0           @result = $p->{code}($request, $record->{$set_field});
1681             }
1682             }
1683            
1684             # If we have a 'lookup' attribute, then use it.
1685            
1686             elsif ( ref $p->{lookup} eq 'HASH' )
1687             {
1688 0 0 0       if ( $p->{from_each} )
    0          
    0          
1689             {
1690 0 0         if ( ref $record->{$source_field} eq 'ARRAY' )
    0          
1691             {
1692 0   0       @result = map { $p->{lookup}{$_} // $p->{default} } @{$record->{$source_field}};
  0            
  0            
1693             }
1694             elsif ( ! ref $record->{$source_field} )
1695             {
1696 0   0       @result = $p->{lookup}{$record->{$source_field}} // $p->{default};
1697             }
1698             }
1699            
1700             elsif ( $p->{from} )
1701             {
1702             @result = $p->{lookup}{$record->{$source_field}} // $p->{default}
1703 0 0 0       unless ref $record->{$source_field};
1704             }
1705            
1706             elsif ( $set_field ne '*' && ! ref $record->{$set_field} )
1707             {
1708 0 0 0       @result = $p->{lookup}{$record->{$set_field}} // $p->{default} if defined $record->{$set_field};
1709             }
1710             }
1711            
1712             # If we have a 'split' attribute, then use it.
1713            
1714             elsif ( defined $p->{split} )
1715             {
1716 0 0         if ( $p->{from_each} )
    0          
    0          
1717             {
1718 0 0         if ( ref $record->{$source_field} eq 'ARRAY' )
    0          
1719             {
1720 0           @result = map { split($p->{split}, $_) } @{$record->{$source_field}};
  0            
  0            
1721             }
1722             elsif ( ! ref $record->{$source_field} )
1723             {
1724 0           @result = split($p->{split}, $record->{$source_field});
1725             }
1726             }
1727            
1728             elsif ( $p->{from} )
1729             {
1730             @result = split $p->{split}, $record->{$source_field}
1731 0 0 0       if defined $record->{$source_field} && ! ref $record->{$source_field};
1732             }
1733            
1734             elsif ( $set_field ne '*' )
1735             {
1736             @result = split $p->{split}, $record->{$set_field}
1737 0 0 0       if defined $record->{$set_field} && ! ref $record->{$set_field};
1738             }
1739             }
1740            
1741             # If we have a 'join' attribute, then use it.
1742            
1743             elsif ( defined $p->{join} )
1744             {
1745 0 0         if ( $source_field )
    0          
1746             {
1747 0           @result = join($p->{join}, @{$record->{$source_field}})
1748 0 0         if ref $record->{$source_field} eq 'ARRAY';
1749             }
1750            
1751             elsif ( $set_field ne '*' )
1752             {
1753 0           @result = join($p->{join}, @{$record->{$set_field}})
1754 0 0         if ref $record->{$set_field} eq 'ARRAY';
1755             }
1756             }
1757            
1758             # Otherwise, we just use the vaoue of the source field.
1759            
1760             else
1761             {
1762             @result = ref $record->{$source_field} eq 'ARRAY' ?
1763 0 0         @{$record->{$source_field}} : $record->{$source_field};
  0            
1764             }
1765            
1766             # If the value of 'set' is '*', then we're done. This is generally
1767             # only used to call a procedure with side effects.
1768            
1769 0 0         next if $set_field eq '*';
1770            
1771             # Otherwise, use the value to modify the specified field of the record.
1772            
1773             # If the attribute 'append' is set, then append to the specified field.
1774             # Convert the value to an array if it isn't already.
1775            
1776 0 0         if ( $p->{append} )
1777             {
1778             $record->{$set_field} = [ $record->{$set_field} ] if defined $record->{$set_field}
1779 0 0 0       and ref $record->{$set_field} ne 'ARRAY';
1780            
1781 0           push @{$record->{$set_field}}, @result;
  0            
1782             }
1783            
1784             else
1785             {
1786 0 0         if ( @result == 1 )
    0          
    0          
1787             {
1788 0           ($record->{$set_field}) = @result;
1789             }
1790            
1791             elsif ( @result > 1 )
1792             {
1793 0           $record->{$set_field} = \@result;
1794             }
1795            
1796             elsif ( not $p->{always} )
1797             {
1798 0           delete $record->{$set_field};
1799             }
1800            
1801             else
1802             {
1803 0           $record->{$set_field} = '';
1804             }
1805             }
1806             }
1807            
1808 0           return 1;
1809             }
1810              
1811              
1812             # check_field_type ( record, field, type, subst )
1813             #
1814             # Make sure that the specified field matches the specified data type. If not,
1815             # substitute the specified value.
1816              
1817             sub check_field_type {
1818              
1819 0     0 0   my ($ds, $record, $field, $type, $subst) = @_;
1820            
1821 0 0         return unless defined $record->{$field};
1822            
1823 0 0         if ( $type eq 'int' )
    0          
    0          
    0          
1824             {
1825 0 0         return if $record->{$field} =~ qr< ^ -? [1-9][0-9]* $ >x;
1826             }
1827            
1828             elsif ( $type eq 'pos' )
1829             {
1830 0 0         return if $record->{$field} =~ qr< ^ [1-9][0-9]* $ >x;
1831             }
1832            
1833             elsif ( $type eq 'dec' )
1834             {
1835 0 0         return if $record->{$field} =~ qr< ^ -? (?: [1-9][0-9]* (?: \. [0-9]* )? | [0]? \. [0-9]+ | [0] \.? ) $ >x;
1836             }
1837            
1838             elsif ( $type eq 'sci' )
1839             {
1840 0 0         return if $record->{$field} =~ qr< ^ -? (?: [1-9][0-9]* \. [0-9]* | [0]? \. [0-9]+ | [0] \. ) (?: [eE] -? [1-9][0-9]* ) $ >x;
1841             }
1842            
1843             # If the data type is something we don't recognize, don't do any check.
1844            
1845             else
1846             {
1847 0           return;
1848             }
1849            
1850             # If we get here, then the value failed the test. If we were given a
1851             # replacement value, substitute it. Otherwise, just delete the field.
1852            
1853 0 0         if ( defined $subst )
1854             {
1855 0           $record->{$field} = $subst;
1856             }
1857            
1858             else
1859             {
1860 0           delete $record->{$field};
1861             }
1862             }
1863              
1864              
1865             # _generate_single_result ( request )
1866             #
1867             # This function is called after an operation is executed and returns a single
1868             # record. Return this record formatted as a single string according to the
1869             # specified output format.
1870              
1871             sub _generate_single_result {
1872              
1873 0     0     my ($ds, $request) = @_;
1874            
1875             # Determine the output format and figure out which class implements it.
1876            
1877 0           my $format = $request->output_format;
1878 0           my $format_class = $ds->{format}{$format}{package};
1879            
1880 0 0         die "could not generate a result in format '$format': no implementing module was found"
1881             unless $format_class;
1882            
1883 0           my $path = $request->node_path;
1884            
1885             # Set the result count to 1, in case the client asked for it.
1886            
1887 0           $request->{result_count} = 1;
1888            
1889             # Get the lists that specify how to process each record and which fields
1890             # to output.
1891            
1892 0           my $proc_list = $request->{proc_list};
1893 0           my $field_list = $request->{field_list};
1894            
1895             # Make sure we have at least one field to output.
1896            
1897 0 0 0       unless ( ref $field_list && @$field_list )
1898             {
1899 0           $request->add_warning("No output fields were defined for this request.");
1900             }
1901            
1902             # If there is a before_record_hook defined for this path, call it now. For a single result,
1903             # calls to 'skip_output_record' are not allowed.
1904            
1905             $ds->_call_hooks($request, 'before_record_hook', $request->{main_record})
1906 0 0         if $request->{hook_enabled}{before_record_hook};
1907            
1908             # If there are any processing steps to do, then do them.
1909            
1910 0           $ds->process_record($request, $request->{main_record}, $proc_list);
1911            
1912             # Generate the initial part of the output, before the first record.
1913            
1914 0           my $header = $format_class->emit_header($request, $field_list);
1915            
1916             # Generate the output corresponding to our single record.
1917            
1918 0           my $record = $format_class->emit_record($request, $request->{main_record}, $field_list);
1919            
1920             # Generate the final part of the output, after the last record.
1921            
1922 0           my $footer = $format_class->emit_footer($request, $field_list);
1923              
1924             # If an after_serialize_hook is defined for this path, call it.
1925            
1926 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
1927             {
1928 0           my $rs = '';
1929            
1930 0           $ds->_call_hooks($request, 'after_serialize_hook', 'header', \$header);
1931 0           $ds->_call_hooks($request, 'after_serialize_hook', 'record', \$rs, \$record);
1932 0           $ds->_call_hooks($request, 'after_serialize_hook', 'footer', \$footer);
1933             }
1934            
1935 0           return $header . $record . $footer;
1936             }
1937              
1938              
1939             # _generate_compound_result ( request )
1940             #
1941             # This function is called after an operation is executed and returns a result
1942             # set, provided that the entire result set does not need to be processed
1943             # before output. It serializes each result record according to the specified output
1944             # format and returns the resulting string. If $streaming_threshold is
1945             # specified, and if the size of the output exceeds this threshold, this
1946             # routine then sets up to stream the rest of the output.
1947              
1948             sub _generate_compound_result {
1949              
1950 0     0     my ($ds, $request, $streaming_threshold) = @_;
1951            
1952             # Determine the output format and figure out which class implements it.
1953            
1954 0           my $format = $request->output_format;
1955 0           my $format_class = $ds->{format}{$format}{package};
1956            
1957 0 0         die "could not generate a result in format '$format': no implementing module was found"
1958             unless $format_class;
1959            
1960 0           my $path = $request->node_path;
1961 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
1962            
1963             # If we have an explicit result list, then we know the count.
1964            
1965 0           $request->{result_count} = scalar(@{$request->{main_result}})
1966 0 0         if ref $request->{main_result};
1967            
1968             # Generate the initial part of the output, before the first record.
1969            
1970 0           my $output = $format_class->emit_header($request, $request->{field_list});
1971            
1972 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
1973             {
1974 0           $ds->_call_hooks($request, 'after_serialize_hook', 'header', \$output);
1975             }
1976            
1977             # A record separator is emitted before every record except the first. If
1978             # this format class does not define a record separator, use the empty
1979             # string.
1980            
1981 0 0         $request->{rs} = $format_class->can('emit_separator') ?
1982             $format_class->emit_separator($request) : '';
1983            
1984 0           my $emit_rs = 0;
1985            
1986 0           $request->{actual_count} = 0;
1987            
1988             # If we have a result limit of 0, just output the header and footer and
1989             # don't bother about the records.
1990            
1991 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} eq '0' )
    0 0        
      0        
1992             {
1993 0           $request->{limit_zero} = 1;
1994             }
1995            
1996             # Otherwise, if an offset was specified and the result method didn't
1997             # handle this itself, then skip the specified number of records.
1998            
1999             elsif ( defined $request->{result_offset} && $request->{result_offset} > 0
2000             && ! $request->{offset_handled} )
2001             {
2002 0           foreach (1..$request->{result_offset})
2003             {
2004 0 0         $ds->_next_record($request) or last;
2005             }
2006             }
2007            
2008             # Now fetch and process each output record in turn. If output streaming is
2009             # available and our total output size exceeds the threshold, switch over
2010             # to streaming.
2011            
2012             RECORD:
2013 0           while ( my $record = $ds->_next_record($request) )
2014             {
2015 0           my $proc_list = $request->{proc_list};
2016 0           my $field_list = $request->{field_list};
2017            
2018             # If there is a before_record_hook defined for this path, call it now.
2019            
2020 0 0         if ( $request->{hook_enabled}{before_record_hook} )
2021             {
2022 0           $ds->_call_hooks($request, 'before_record_hook', $record);
2023             }
2024              
2025             # If 'skip_output_record' was called on this record, then skip it now. If
2026             # 'select_output_block' was called, then substitute the field list and proc list associated
2027             # with that block.
2028              
2029 0 0         next RECORD if $record->{_skip_record};
2030            
2031 0 0         if ( my $alt = $record->{_output_block} )
2032             {
2033 0           $proc_list = $request->{block_proc_list}{$alt};
2034 0           $field_list = $request->{block_field_list}{$alt};
2035             }
2036            
2037             # If there are any processing steps to do, then process this record.
2038            
2039 0           $ds->process_record($request, $record, $proc_list);
2040            
2041             # Generate the output for this record, preceded by a record separator if
2042             # it is not the first record.
2043            
2044 0 0         my $outrs = $emit_rs ? $request->{rs} : ''; $emit_rs = 1;
  0            
2045 0           my $outrec = $format_class->emit_record($request, $record, $field_list);
2046            
2047 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2048             {
2049 0           $ds->_call_hooks($request, 'after_serialize_hook', 'record', \$outrs, \$outrec);
2050             }
2051            
2052 0           $output .= $outrs . $outrec;
2053            
2054             # Keep count of the output records, and stop if we have exceeded the
2055             # limit.
2056            
2057 0           $request->{actual_count}++;
2058            
2059 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} ne 'all' )
2060             {
2061 0 0         last if $request->{actual_count} >= $request->{result_limit};
2062             }
2063            
2064             # If streaming is a possibility, check whether we have passed the
2065             # threshold for result size. If so, then we need to immediately
2066             # stash the output generated so far and call stream_data. Doing that
2067             # will cause the current function to be aborted, followed by an
2068             # automatic call to &stream_result (defined below).
2069            
2070 0 0 0       if ( defined $streaming_threshold && length($output) > $streaming_threshold )
2071             {
2072 0           $request->{stashed_output} = $output;
2073 0           Dancer::Plugin::StreamData::stream_data($request, &_stream_compound_result);
2074             }
2075             }
2076            
2077             # If we get here, then we did not initiate streaming. So add the
2078             # footer and return the output data.
2079            
2080             # If we didn't output any records, give the formatter a chance to indicate
2081             # this.
2082            
2083 0 0         unless ( $request->{actual_count} )
2084             {
2085 0           my $empty = $format_class->emit_empty($request);
2086            
2087 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2088             {
2089 0           $ds->_call_hooks($request, 'after_serialize_hook', 'empty', \$empty);
2090             }
2091              
2092 0           $output .= $empty;
2093             }
2094            
2095             # Generate the final part of the output, after the last record.
2096            
2097 0           my $footer = $format_class->emit_footer($request, $request->{field_list});
2098              
2099 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2100             {
2101 0           $ds->_call_hooks($request, 'after_serialize_hook', 'footer', \$footer);
2102             }
2103            
2104 0           $output .= $footer;
2105            
2106             # Determine if we need to encode the output into the proper character set.
2107             # Usually Dancer does this for us, but only if it recognizes the content
2108             # type as text. For these formats, the definition should set the
2109             # attribute 'encode_as_text' to true.
2110            
2111 0           my $output_charset = $ds->{_config}{charset};
2112 0           my $must_encode;
2113            
2114 0 0 0       if ( $output_charset
      0        
2115             && $ds->{format}{$format}{encode_as_text}
2116             && ! $request->{content_type_is_text} )
2117             {
2118 0           $must_encode = 1;
2119             }
2120            
2121 0 0         return $must_encode ? encode($output_charset, $output) : $output;
2122             }
2123              
2124              
2125             # _generate_processed_result ( request )
2126             #
2127             # This function is called if the result set needs to be processed in its
2128             # entirety before being output. It processes the entire result set and
2129             # collects a list of processed records, and then serializes each result record
2130             # according to the specified output format. If $streaming_threshold is
2131             # specified, and if the size of the output exceeds this threshold, this
2132             # routine then sets up to stream the rest of the output.
2133              
2134             sub _generate_processed_result {
2135              
2136 0     0     my ($ds, $request, $streaming_threshold) = @_;
2137            
2138             # Determine the output format and figure out which class implements it.
2139            
2140 0           my $format = $request->output_format;
2141 0           my $format_class = $ds->{format}{$format}{package};
2142            
2143 0 0         die "could not generate a result in format '$format': no implementing module was found"
2144             unless $format_class;
2145            
2146 0           $ds->debug_line("Processing result set before output.");
2147            
2148 0           my $path = $request->node_path;
2149 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
2150            
2151             # Now fetch and process each output record in turn. Collect up all of the
2152             # records that pass the processing phase in a list.
2153            
2154 0           my @results;
2155            
2156             RECORD:
2157 0           while ( my $record = $ds->_next_record($request) )
2158             {
2159 0           my $proc_list = $request->{proc_list};
2160 0           my $field_list = $request->{field_list};
2161            
2162             # If there is a before_record_hook defined for this path, call it now.
2163            
2164 0 0         if ( $request->{hook_enabled}{before_record_hook} )
2165             {
2166 0           $ds->_call_hooks($request, 'before_record_hook', $record);
2167             }
2168              
2169             # If 'skip_output_record' was called on this record, skip it now. If 'select_output_block'
2170             # was called, then substitute the proc list associated with the selected output block.
2171            
2172 0 0         next RECORD if $record->{_skip_record};
2173            
2174 0 0         if ( $record->{_output_block} )
2175             {
2176 0           $proc_list = $request->{block_proc_list}{$record->{_output_block}};
2177             }
2178            
2179             # If there are any processing steps to do, then process this record.
2180            
2181 0           $ds->process_record($request, $record, $proc_list);
2182            
2183             # Add the record to the list.
2184            
2185 0           push @results, $record;
2186             }
2187            
2188             # We now know the result count.
2189            
2190 0           $request->{result_count} = scalar(@results);
2191            
2192             # At this point, we can generate the output. We start with the header.
2193            
2194 0           my $output = $format_class->emit_header($request, $request->{field_list});
2195            
2196 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2197             {
2198 0           $ds->_call_hooks($request, 'after_serialize_hook', 'header', \$output);
2199             }
2200            
2201             # A record separator is emitted before every record except the first. If
2202             # this format class does not define a record separator, use the empty
2203             # string.
2204            
2205 0 0         $request->{rs} = $format_class->can('emit_separator') ?
2206             $format_class->emit_separator($request) : '';
2207            
2208 0           my $emit_rs = 0;
2209            
2210 0           $request->{actual_count} = 0;
2211            
2212             # If an offset was specified and the result method didn't handle this
2213             # itself, then skip the specified number of records.
2214            
2215 0 0 0       if ( defined $request->{result_offset} && $request->{result_offset} > 0
      0        
2216             && ! $request->{offset_handled} )
2217             {
2218 0           splice(@results, 0, $request->{result_offset});
2219             }
2220            
2221             # If the result limit is zero, we can ignore all records.
2222            
2223 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} eq '0' )
2224             {
2225 0           @results = ();
2226             }
2227            
2228             # Otherwise iterate over all of the remaining records.
2229            
2230             OUTPUT:
2231 0           while ( @results )
2232             {
2233 0           my $record = shift @results;
2234              
2235             # If an alternate block was specified for this record, use it.
2236              
2237             my $field_list = $record->{block_field_list}{$record->{_output_block}}
2238 0 0         if $record->{_output_block};
2239            
2240             # Generate the output for this record, preceded by a record separator if
2241             # it is not the first record.
2242            
2243 0 0         my $outrs = $emit_rs ? $request->{rs} : ''; $emit_rs = 1;
  0            
2244 0   0       my $outrec = $format_class->emit_record($request, $record, $field_list || $request->{field_list});
2245            
2246 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2247             {
2248 0           $ds->_call_hooks($request, 'after_serialize_hook', 'record', \$outrs, \$outrec);
2249             }
2250            
2251 0           $output .= $outrs . $outrec;
2252            
2253             # Keep count of the output records, and stop if we have exceeded the
2254             # limit.
2255            
2256 0           $request->{actual_count}++;
2257            
2258 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} ne 'all' )
2259             {
2260 0 0         last if $request->{actual_count} >= $request->{result_limit};
2261             }
2262            
2263             # If streaming is a possibility, check whether we have passed the
2264             # threshold for result size. If so, then we need to immediately
2265             # stash the output generated so far and call stream_data. Doing that
2266             # will cause the current function to be aborted, followed by an
2267             # automatic call to &stream_result (defined below).
2268            
2269 0 0 0       if ( defined $streaming_threshold && length($output) > $streaming_threshold )
2270             {
2271 0           $request->{stashed_output} = $output;
2272 0           $request->{stashed_results} = \@results;
2273 0           $request->{processing_complete} = 1;
2274 0           Dancer::Plugin::StreamData::stream_data($request, &_stream_compound_result);
2275             }
2276             }
2277            
2278             # If we get here, then we did not initiate streaming. So add the
2279             # footer and return the output data.
2280            
2281             # If we didn't output any records, give the formatter a chance to indicate
2282             # this.
2283            
2284 0 0         unless ( $request->{actual_count} )
2285             {
2286 0           my $empty = $format_class->emit_empty($request);
2287            
2288 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2289             {
2290 0           $ds->_call_hooks($request, 'after_serialize_hook', 'empty', \$empty);
2291             }
2292            
2293 0           $output .= $empty;
2294             }
2295            
2296             # Generate the final part of the output, after the last record.
2297            
2298 0           my $footer = $format_class->emit_footer($request, $request->{field_list});
2299            
2300 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2301             {
2302 0           $ds->_call_hooks($request, 'after_serialize_hook', 'footer', \$footer);
2303             }
2304            
2305 0           $output .= $footer;
2306            
2307             # Determine if we need to encode the output into the proper character set.
2308             # Usually Dancer does this for us, but only if it recognizes the content
2309             # type as text. For these formats, the definition should set the
2310             # attribute 'encode_as_text' to true.
2311            
2312 0           my $output_charset = $ds->{_config}{charset};
2313 0           my $must_encode;
2314            
2315 0 0 0       if ( $output_charset
      0        
2316             && $ds->{format}{$format}{encode_as_text}
2317             && ! $request->{content_type_is_text} )
2318             {
2319 0           $must_encode = 1;
2320             }
2321            
2322 0 0         return $must_encode ? encode($output_charset, $output) : $output;
2323             }
2324              
2325              
2326             # _stream_compound_result ( )
2327             #
2328             # Continue to generate a compound query result from where
2329             # generate_compound_result() left off, and stream it to the client
2330             # record-by-record.
2331             #
2332             # This routine must be passed a Plack 'writer' object, to which will be
2333             # written in turn the stashed output from generate_compound_result(), each
2334             # subsequent record, and then the footer. Each of these chunks of data will
2335             # be immediately sent off to the client, instead of being marshalled together
2336             # in memory. This allows the server to send results up to hundreds of
2337             # megabytes in length without bogging down.
2338              
2339             sub _stream_compound_result {
2340            
2341 0     0     my ($request, $writer) = @_;
2342            
2343 0           my $ds = $request->{ds};
2344            
2345             # Determine the output format and figure out which class implements it.
2346            
2347 0           my $format = $request->output_format;
2348 0           my $format_class = $ds->{format}{$format}{package};
2349 0           my $format_is_text = $ds->{format}{$format}{is_text};
2350            
2351 0 0         croak "could not generate a result in format '$format': no implementing class"
2352             unless $format_class;
2353            
2354 0           my $path = $request->node_path;
2355 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
2356            
2357             # Determine the output character set, because we will need to encode text
2358             # responses in it.
2359            
2360 0           my $output_charset = $ds->{_config}{charset};
2361            
2362             #return $must_encode ? encode($output_charset, $output) : $output;
2363            
2364             # First send out the partial output previously stashed by
2365             # generate_compound_result().
2366            
2367 0 0 0       if ( $output_charset && $format_is_text )
2368             {
2369 0           $writer->write( encode($output_charset, $ds->{stashed_output}) );
2370             }
2371            
2372             else
2373             {
2374 0           $writer->write( $ds->{stashed_output} );
2375             }
2376            
2377             # Then process the remaining rows.
2378            
2379             RECORD:
2380 0           while ( my $record = $ds->_next_record($request) )
2381             {
2382 0           my $proc_list = $request->{proc_list};
2383 0           my $field_list = $request->{field_list};
2384            
2385             # If there are any processing steps to do, then process this record. But skip this if this
2386             # subroutine was called from '_generate_processed_result'.
2387              
2388 0 0         if ( $request->{processing_complete} )
2389             {
2390             $field_list = $record->{block_field_list}{$record->{_output_block}}
2391 0 0         if $record->{_output_block};
2392             }
2393            
2394             else
2395             {
2396             # If there is a before_record_hook defined for this path, call it now.
2397            
2398 0 0         if ( $request->{hook_enabled}{before_record_hook} )
2399             {
2400 0           $ds->_call_hooks($request, 'before_record_hook', $record);
2401             }
2402              
2403             # If 'skip_output_record' was called on this record, skip it now. If
2404             # 'select_output_block' was called, then substitute the proc list and field list for
2405             # the selected block.
2406            
2407 0 0         next RECORD if $record->{_skip_record};
2408            
2409 0 0         if ( my $alt = $record->{_output_block} )
2410             {
2411 0           $proc_list = $request->{block_proc_list}{$alt};
2412 0           $field_list = $request->{block_field_list}{$alt};
2413             }
2414            
2415             # Do any processing steps that were defined for this record.
2416            
2417 0           $ds->process_record($request, $record, $proc_list);
2418             }
2419            
2420             # Generate the output for this record, preceded by a record separator if
2421             # it is not the first record.
2422            
2423 0           my $outrs = $request->{rs};
2424 0           my $outrec = $format_class->emit_record($request, $record, $field_list);
2425            
2426 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2427             {
2428 0           $ds->_call_hooks($request, 'after_serialize_hook', 'record', \$outrs, \$outrec);
2429             }
2430            
2431 0           my $output .= $outrs . $outrec;
2432            
2433 0 0 0       if ( ! defined $output or $output eq '' )
    0 0        
2434             {
2435             # do nothing
2436             }
2437            
2438             elsif ( $output_charset && $format_is_text )
2439             {
2440 0           $writer->write( encode($output_charset, $output) );
2441             }
2442            
2443             else
2444             {
2445 0           $writer->write( $output );
2446             }
2447            
2448             # Keep count of the output records, and stop if we have exceeded the
2449             # limit.
2450            
2451             last if $request->{result_limit} ne 'all' &&
2452 0 0 0       ++$request->{actual_count} >= $request->{result_limit};
2453             }
2454            
2455             # finish output...
2456            
2457             # my $final = $ds->finishOutput();
2458             # $writer->write( encode_utf8($final) ) if defined $final and $final ne '';
2459            
2460             # Finally, send out the footer and then close the writer object.
2461            
2462             # Generate the final part of the output, after the last record.
2463            
2464 0           my $footer = $format_class->emit_footer($request, $request->{field_list});
2465            
2466 0 0         if ( $request->{hook_enabled}{after_serialize_hook} )
2467             {
2468 0           $ds->_call_hooks($request, 'after_serialize_hook', 'footer', \$footer);
2469             }
2470            
2471 0 0 0       if ( ! defined $footer or $footer eq '' )
    0 0        
2472             {
2473             # do nothing
2474             }
2475            
2476             elsif ( $output_charset && $format_is_text )
2477             {
2478 0           $writer->write( encode($output_charset, $footer) );
2479             }
2480            
2481             else
2482             {
2483 0           $writer->write( $footer );
2484             }
2485            
2486 0           $writer->close();
2487             }
2488              
2489              
2490             # _next_record ( request )
2491             #
2492             # Return the next record to be output for the given request. If
2493             # $ds->{main_result} is set, use that first. Once that is exhausted (or if
2494             # it was never set) then if $result->{main_sth} is set then read records from
2495             # it until exhausted.
2496              
2497             sub _next_record {
2498            
2499 0     0     my ($ds, $request) = @_;
2500            
2501             # If the request has a zero limit, and no processing needs to be done on
2502             # the result set, then no records need to be returned.
2503            
2504 0 0         return if $request->{limit_zero};
2505            
2506             # If we have a stashed result list, return the next item in it.
2507            
2508 0 0 0       if ( ref $request->{stashed_results} eq 'ARRAY' )
    0          
    0          
2509             {
2510 0           return shift @{$request->{stashed_results}};
  0            
2511             }
2512            
2513             # If we have a 'main_result' array with something in it, return the next
2514             # item in it.
2515            
2516 0           elsif ( ref $request->{main_result} eq 'ARRAY' and @{$request->{main_result}} )
2517             {
2518 0           return shift @{$request->{main_result}};
  0            
2519             }
2520            
2521             # Otherwise, if we have a 'main_sth' statement handle, read the next item
2522             # from it.
2523            
2524             elsif ( ref $request->{main_sth} )
2525             {
2526             return $request->{main_sth}->fetchrow_hashref
2527 0           }
2528            
2529             else
2530             {
2531 0           return;
2532             }
2533             }
2534              
2535              
2536             # _generate_empty_result ( request )
2537             #
2538             # This function is called after an operation is executed and returns no results
2539             # at all. Return the header and footer only.
2540              
2541             sub _generate_empty_result {
2542            
2543 0     0     my ($ds, $request) = @_;
2544            
2545             # Determine the output format and figure out which class implements it.
2546            
2547 0           my $format = $request->output_format;
2548 0           my $format_class = $ds->{format}{$format}{package};
2549            
2550 0 0         croak "could not generate a result in format '$format': no implementing class"
2551             unless $format_class;
2552            
2553             # Call the appropriate methods from this class to generate the header,
2554             # and footer.
2555            
2556 0           my $output = $format_class->emit_header($request);
2557            
2558 0           $output .= $format_class->emit_empty($request);
2559 0           $output .= $format_class->emit_footer($request);
2560            
2561 0           return $output;
2562             }
2563              
2564              
2565             1;