File Coverage

lib/Web/DataService/Output.pm
Criterion Covered Total %
statement 76 752 10.1
branch 23 624 3.6
condition 5 353 1.4
subroutine 10 31 32.2
pod 0 19 0.0
total 114 1779 6.4


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         6  
  2         81  
10              
11             package Web::DataService::Output;
12              
13 2     2   18 use Encode;
  2         3  
  2         166  
14 2     2   14 use Scalar::Util qw(reftype);
  2         4  
  2         80  
15 2     2   12 use Carp qw(carp croak);
  2         4  
  2         108  
16              
17 2     2   14 use Moo::Role;
  2         3  
  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         2 my $name = shift;
35            
36             # Check to make sure that we were given a valid name.
37            
38 1 50       6 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       11 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         6 my ($package, $filename, $line) = caller;
59 1         6 $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         7 $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         7 $ds->add_doc($block, $item);
81 2         7 next;
82             }
83            
84             # Any item that is not a hashref is an error.
85            
86 2 50       6 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         6 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       10 $ds->add_doc($block, $item) if $type eq 'output';
99            
100             # Add the record to the appropriate list(s).
101            
102 2 50       13 if ( $type eq 'include' )
103             {
104 0         0 push @{$ds->{block}{$name}{include_list}}, $item;
  0         0  
105             }
106            
107 2         3 push @{$ds->{block}{$name}{output_list}}, $item;
  2         8  
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         5 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       12 croak "define_output: you cannot have both attributes '$type' and '$k' in one record"
186             if $type;
187            
188 2         7 $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       6 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         5 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          
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 ne 'output' )
848             {
849 0         0 warn "Warning: unknown key '$key' in output record\n";
850             }
851             }
852            
853 0         0 push @field_list, $output;
854             }
855            
856             # If the record type is 'set', add a record to the proc list.
857            
858             elsif ( defined $r->{set} )
859             {
860 0         0 my $proc = { set => $r->{set} };
861            
862 0         0 foreach my $key ( keys %$r )
863             {
864 0 0       0 if ( $PROC_KEY{$key} )
865             {
866 0         0 $proc->{$key} = $r->{$key};
867             }
868            
869             else
870             {
871 0         0 carp "Warning: unknown key '$key' in proc record\n";
872             }
873             }
874            
875 0         0 push @proc_list, $proc;
876             }
877            
878             # All other record types are ignored.
879             }
880            
881             # Now cache the results.
882            
883 0         0 $request->{block_field_list}{$block_name} = \@field_list;
884 0         0 $request->{block_proc_list}{$block_name} = \@proc_list;
885            
886 0         0 return 1;
887             }
888              
889              
890             # check_value ( list, value )
891             #
892             # Return true if $list is equal to $value, or if it is a list and one if its
893             # items is equal to $value.
894              
895             sub check_value {
896            
897 0     0 0 0 my ($list, $value) = @_;
898            
899 0 0       0 return 1 if $list eq $value;
900            
901 0 0       0 if ( ref $list eq 'ARRAY' )
902             {
903 0         0 foreach my $item (@$list)
904             {
905 0 0       0 return 1 if $item eq $value;
906             }
907             }
908            
909 0         0 return;
910             }
911              
912              
913             # check_set ( list, set )
914             #
915             # The parameter $set must be a hashref. Return true if $list is one of the
916             # keys of $set, or if it $list is a list and one of its items is a key in
917             # $set. A key only counts if it has a true value.
918              
919             sub check_set {
920            
921 0     0 0 0 my ($list, $set) = @_;
922            
923 0 0       0 return unless ref $set eq 'HASH';
924            
925 0 0       0 return 1 if $set->{$list};
926            
927 0 0       0 if ( ref $list eq 'ARRAY' )
928             {
929 0         0 foreach my $item (@$list)
930             {
931 0 0       0 return 1 if $set->{$item};
932             }
933             }
934            
935 0         0 return;
936             }
937              
938              
939             # add_doc ( node, item )
940             #
941             # Add the specified item to the documentation list for the specified node.
942             # The item can be either a string or a record (hashref).
943              
944             sub add_doc {
945              
946 8     8 0 17 my ($ds, $node, $item) = @_;
947            
948             # If the item is a record, close any currently pending documentation and
949             # start a new "pending" list. We need to do this because subsequent items
950             # may document the record we were just called with.
951            
952 8 100       36 if ( ref $item )
    50          
953             {
954 4 50       14 croak "cannot add non-hash object to documentation"
955             unless reftype $item eq 'HASH';
956            
957 4         11 $ds->process_doc($node);
958 4         6 push @{$node->{doc_pending}}, $item;
  4         10  
959             }
960            
961             # If this is a string starting with one of the special characters, then
962             # handle it properly.
963            
964             elsif ( $item =~ qr{ ^ ([!^?] | >>?) (.*) }xs )
965             {
966             # If >>, then close the active documentation section (if any) and
967             # start a new one that is not tied to any rule. This will generate an
968             # ordinary paragraph starting with the remainder of the line.
969            
970 0 0       0 if ( $1 eq '>>' )
    0          
    0          
971             {
972 0         0 $ds->process_doc($node);
973 0 0       0 push @{$node->{doc_pending}}, $2 if $2 ne '';
  0         0  
974             }
975            
976             # If >, then add to the current documentation a blank line
977             # (which will cause a new paragraph) followed by the remainder
978             # of this line.
979            
980             elsif ( $1 eq '>' )
981             {
982 0         0 push @{$node->{doc_pending}}, "\n$2";
  0         0  
983             }
984            
985             # If !, then discard all pending documentation and mark the node as
986             # 'undocumented'. This will cause it to be elided from the documentation.
987            
988             elsif ( $1 eq '!' )
989             {
990 0         0 $ds->process_doc($node, 'undocumented');
991             }
992            
993             # If ?, then add the remainder of the line to the documentation.
994             # The ! prevents the next character from being interpreted specially.
995            
996             else
997             {
998 0         0 push @{$node->{doc_pending}}, $2;
  0         0  
999             }
1000             }
1001            
1002             # Otherwise, just add this string to the "pending" list.
1003            
1004             else
1005             {
1006 4         8 push @{$node->{doc_pending}}, $item;
  4         19  
1007             }
1008             }
1009              
1010              
1011             # process_doc ( node, disposition )
1012             #
1013             # Process all pending documentation items.
1014              
1015             sub process_doc {
1016              
1017 6     6 0 12 my ($ds, $node, $disposition) = @_;
1018            
1019             # Return immediately unless we have something pending.
1020            
1021 6 100 66     23 return unless ref $node->{doc_pending} eq 'ARRAY' && @{$node->{doc_pending}};
  4         13  
1022            
1023             # If the "pending" list starts with an item record, take that off first.
1024             # Everything else on the list should be a string.
1025            
1026 4         6 my $primary_item = shift @{$node->{doc_pending}};
  4         8  
1027 4 50       10 return unless ref $primary_item;
1028            
1029             # Discard all pending documentation if the primary item is disabled or
1030             # marked with a '!'. In the latter case, note this in the item record.
1031            
1032 4   50     17 $disposition //= '';
1033            
1034 4 50 33     22 if ( $primary_item->{disabled} or $primary_item->{undocumented} or
      33        
1035             $disposition eq 'undocumented' )
1036             {
1037 0         0 @{$node->{doc_pending}} = ();
  0         0  
1038 0 0       0 $primary_item->{undocumented} = 1 if $disposition eq 'undocumented';
1039 0         0 return;
1040             }
1041            
1042             # Put the rest of the documentation items together into a single
1043             # string, which may contain a series of Pod paragraphs.
1044            
1045 4         9 my $body = '';
1046 4         6 my $last_pod;
1047             my $this_pod;
1048            
1049 4         7 while (my $line = shift @{$node->{doc_pending}})
  8         19  
1050             {
1051             # If this line starts with =, then it needs extra spacing.
1052            
1053 4         15 my $this_pod = $line =~ qr{ ^ = }x;
1054            
1055             # If $body already has something in it, add a newline first. Add
1056             # two if this line starts with =, or if the previously added line
1057             # did, so that we get a new paragraph.
1058            
1059 4 50       11 if ( $body ne '' )
1060             {
1061 0 0 0     0 $body .= "\n" if $last_pod || $this_pod;
1062 0         0 $body .= "\n";
1063             }
1064            
1065 4         7 $body .= $line;
1066 4         10 $last_pod = $this_pod;
1067             }
1068            
1069             # Then add the documentation to the node's documentation list. If there
1070             # is no primary item, add the body as an ordinary paragraph.
1071            
1072 4 50       8 unless ( defined $primary_item )
1073             {
1074 0         0 push @{$node->{doc_list}}, clean_doc($body);
  0         0  
1075             }
1076            
1077             # Otherwise, attach the body to the primary item and add it to the list.
1078            
1079             else
1080             {
1081 4         10 $primary_item->{doc_string} = clean_doc($body, 1);
1082 4         8 push @{$node->{doc_list}}, $primary_item;
  4         12  
1083             }
1084             }
1085              
1086              
1087             # clean_doc ( )
1088             #
1089             # Make sure that the indicated string is valid POD. In particular, if there
1090             # are any unclosed =over sections, close them at the end. Throw an exception
1091             # if we find an =item before the first =over or a =head inside an =over.
1092              
1093             sub clean_doc {
1094              
1095 4     4 0 7 my ($docstring, $item_body) = @_;
1096            
1097 4         14 my $list_level = 0;
1098            
1099 4         13 while ( $docstring =~ / ^ (=[a-z]+) /gmx )
1100             {
1101 0 0       0 if ( $1 eq '=over' )
    0          
    0          
    0          
1102             {
1103 0         0 $list_level++;
1104             }
1105            
1106             elsif ( $1 eq '=back' )
1107             {
1108 0         0 $list_level--;
1109 0 0       0 croak "invalid POD string: =back does not match any =over" if $list_level < 0;
1110             }
1111            
1112             elsif ( $1 eq '=item' )
1113             {
1114 0 0       0 croak "invalid POD string: =item outside of =over" if $list_level == 0;
1115             }
1116            
1117             elsif ( $1 eq '=head' )
1118             {
1119 0 0 0     0 croak "invalid POD string: =head inside =over" if $list_level > 0 || $item_body;
1120             }
1121             }
1122            
1123 4         9 $docstring .= "\n\n=back" x $list_level;
1124            
1125 4         11 return $docstring;
1126             }
1127              
1128              
1129             # document_node ( node, state )
1130             #
1131             # Return a documentation string for the given node, in Pod format. This will
1132             # consist of a main item list that may start and stop, possibly with ordinary
1133             # Pod paragraphs in between list chunks. If this node contains any 'include'
1134             # records, the lists for those nodes will be recursively interpolated into the
1135             # main list. Sublists can only occur if they are explicitly included in the
1136             # documentation strings for individual node records.
1137             #
1138             # If the $state parameter is given, it must be a hashref containing any of the
1139             # following keys:
1140             #
1141             # namespace A hash ref in which included nodes may be looked up by name.
1142             # If this is not given, then 'include' records are ignored.
1143             #
1144             # items_only If true, then ordinary paragraphs will be ignored and a single
1145             # uninterrupted item list will be generated.
1146             #
1147              
1148             sub document_node {
1149            
1150 0     0 0   my ($ds, $node, $state) = @_;
1151            
1152             # Return the empty string unless documentation has been added to this
1153             # node.
1154            
1155 0 0 0       return '' unless ref $node && ref $node->{doc_list} eq 'ARRAY';
1156            
1157             # Make sure we have a state record, if we were not passed one.
1158            
1159 0   0       $state ||= {};
1160            
1161             # Make sure that we process each node only once, if it should happen
1162             # to be included multiple times. Also keep track of our recursion level.
1163            
1164 0 0         return if $state->{processed}{$node->{name}};
1165            
1166 0           $state->{processed}{$node->{name}} = 1;
1167 0           $state->{level}++;
1168            
1169             # Go through the list of documentation items, treating each one as a Pod
1170             # paragraph. That means that they will be separated from each other by a
1171             # blank line. List control paragraphs "=over" and "=back" will be added
1172             # as necessary to start and stop the main item list.
1173            
1174 0           my $doc = '';
1175            
1176             ITEM:
1177 0           foreach my $item ( @{$node->{doc_list}} )
  0            
1178             {
1179             # A string is added as an ordinary paragraph. The main list is closed
1180             # if it is open. But the item is skipped if we were given the
1181             # 'items_only' flag.
1182            
1183 0 0         unless ( ref $item )
    0          
1184             {
1185 0 0         next ITEM if $state->{items_only};
1186            
1187 0 0         if ( $state->{in_list} )
1188             {
1189 0 0         $doc .= "\n\n" if $doc ne '';
1190 0           $doc .= "=back";
1191 0           $state->{in_list} = 0;
1192             }
1193            
1194 0 0 0       $doc .= "\n\n" if $doc ne '' && $item ne '';
1195 0           $doc .= $item;
1196             }
1197            
1198             # An 'include' record inserts the documentation for the specified
1199             # node. This does not necessarily end the list, only if the include
1200             # record itself has a documentation string. Skip the inclusion if no
1201             # hashref was provided for looking up item names.
1202            
1203 0           elsif ( defined $item->{include} )
1204             {
1205 0 0 0       next ITEM unless ref $state->{namespace} && reftype $state->{namespace} eq 'HASH';
1206            
1207 0 0 0       if ( defined $item->{doc_string} and $item->{doc_string} ne '' and not $state->{items_only} )
      0        
1208             {
1209 0 0         if ( $state->{in_list} )
1210             {
1211 0 0         $doc .= "\n\n" if $doc ne '';
1212 0           $doc .= "=back";
1213 0           $state->{in_list} = 0;
1214             }
1215            
1216 0 0         $doc .= "\n\n" if $doc ne '';
1217 0           $doc .= $item->{doc_string};
1218             }
1219            
1220 0           my $included_node = $state->{namespace}{$item->{include}};
1221            
1222 0 0 0       next unless ref $included_node && reftype $included_node eq 'HASH';
1223            
1224 0           my $subdoc = $ds->document_node($included_node, $state);
1225            
1226 0 0 0       $doc .= "\n\n" if $doc ne '' && $subdoc ne '';
1227 0           $doc .= $subdoc;
1228             }
1229            
1230             # Any other record is added as a list item. Try to figure out the
1231             # item name as best we can.
1232            
1233             else
1234             {
1235             my $name = ref $node eq 'Web::DataService::Set' ? $item->{value}
1236             : defined $item->{name} ? $item->{name}
1237 0 0         : '';
    0          
1238            
1239 0   0       $name ||= '';
1240            
1241 0 0         unless ( $state->{in_list} )
1242             {
1243 0 0         $doc .= "\n\n" if $doc ne '';
1244 0           $doc .= "=over";
1245 0           $state->{in_list} = 1;
1246             }
1247            
1248 0           $doc .= "\n\n=item $name";
1249 0 0 0       $doc .= "\n\n$item->{doc_string}" if defined $item->{doc_string} && $item->{doc_string} ne '';
1250             }
1251             }
1252            
1253             # If we get to the end of the top-level ruleset and we are still in a
1254             # list, close it. Also make sure that our resulting documentation string
1255             # ends with a newline.
1256            
1257 0 0         if ( --$state->{level} == 0 )
1258             {
1259 0 0         $doc .= "\n\n=back" if $state->{in_list};
1260 0           $state->{in_list} = 0;
1261 0           $doc .= "\n";
1262             }
1263            
1264 0           return $doc;
1265             }
1266              
1267              
1268             # document_response ( )
1269             #
1270             # Generate documentation in Pod format describing the available output fields
1271             # for the specified URL path.
1272              
1273             sub document_response {
1274            
1275 0     0 0   my ($ds, $path) = @_;
1276            
1277 0           my @blocks;
1278             my @labels;
1279            
1280             # First collect up a list of all of the fixed (non-optional) blocks.
1281             # Block names that do not correspond to any defined block are ignored,
1282             # with a warning.
1283            
1284 0   0       my $output_list = $ds->node_attr($path, 'output') // [ ];
1285 0   0       my $fixed_label = $ds->node_attr($path, 'output_label') // 'basic';
1286            
1287 0           foreach my $block_name ( @$output_list )
1288             {
1289 0 0         if ( ref $ds->{block}{$block_name} eq 'Web::DataService::Block' )
    0          
1290             {
1291 0           push @blocks, $block_name;
1292 0           push @labels, $fixed_label;
1293             }
1294            
1295             elsif ( $ds->debug )
1296             {
1297             warn "WARNING: block '$block_name' not found"
1298 0 0 0       unless $Web::DataService::QUIET || $ENV{WDS_QUIET};
1299             }
1300             }
1301            
1302             # Then add all of the optional blocks, if an output_opt map was
1303             # specified.
1304            
1305 0           my $optional_output = $ds->node_attr($path, 'optional_output');
1306 0           my $reverse_map;
1307            
1308 0 0 0       if ( $optional_output && ref $ds->{set}{$optional_output} eq 'Web::DataService::Set' )
    0 0        
1309             {
1310 0           my $output_map = $ds->{set}{$optional_output};
1311 0 0         my @keys; @keys = @{$output_map->{value_list}} if ref $output_map->{value_list} eq 'ARRAY';
  0            
  0            
1312            
1313             VALUE:
1314 0           foreach my $label ( @keys )
1315             {
1316 0           my $block_name = $output_map->{value}{$label}{maps_to};
1317 0 0         next VALUE unless defined $block_name;
1318             next VALUE if $output_map->{value}{$label}{disabled} ||
1319 0 0 0       $output_map->{value}{$label}{undocumented};
1320            
1321 0           $reverse_map->{$block_name} = $label;
1322            
1323 0 0         if ( ref $ds->{block}{$block_name} eq 'Web::DataService::Block' )
1324             {
1325 0           push @blocks, $block_name;
1326 0           push @labels, $label;
1327             }
1328             }
1329             }
1330            
1331             elsif ( $optional_output && $ds->debug )
1332             {
1333             warn "WARNING: output map '$optional_output' not found"
1334 0 0 0       unless $Web::DataService::QUIET || $ENV{WDS_QUIET};
1335             }
1336            
1337             # If there are no output blocks specified for this path, return an empty
1338             # string.
1339            
1340 0 0         return '' unless @blocks;
1341            
1342             # Otherwise, determine the set of vocabularies that are allowed for this
1343             # path. If none are specifically selected for this path, then all of the
1344             # vocabularies defined for this data service are allowed.
1345            
1346 0   0       my $vocabularies; $vocabularies = $ds->node_attr($path, 'allow_vocab') || $ds->{vocab};
  0            
1347            
1348 0 0 0       unless ( ref $vocabularies eq 'HASH' && keys %$vocabularies )
1349             {
1350 0 0         warn "No output vocabularies were selected for path '$path'" if $ds->debug;
1351 0           return '';
1352             }
1353            
1354             my @vocab_list = grep { $vocabularies->{$_} &&
1355             ref $ds->{vocab}{$_} &&
1356 0 0 0       ! $ds->{vocab}{$_}{disabled} } @{$ds->{vocab_list}};
  0            
  0            
1357            
1358 0 0         unless ( @vocab_list )
1359             {
1360 0 0         warn "No output vocabularies were selected for path '$path'" if $ds->debug;
1361 0           return "";
1362             }
1363            
1364             # Now generate the header for the documentation, in Pod format. We
1365             # include the special "=for wds_table_header" line to give PodParser.pm the
1366             # information it needs to generate an HTML table.
1367            
1368 0           my $doc_string = '';
1369 0           my $field_count = scalar(@vocab_list);
1370 0           my $field_string = join ' / ', @vocab_list;
1371            
1372 0 0         if ( $field_count > 1 )
1373             {
1374 0           $doc_string .= "=for wds_table_header Field name*/$field_count | Block | Description\n\n";
1375 0           $doc_string .= "=over 4\n\n";
1376 0           $doc_string .= "=item $field_string\n\n";
1377             }
1378            
1379             else
1380             {
1381 0           $doc_string .= "=for wds_table_header Field name* | Block | Description\n\n";
1382 0           $doc_string .= "=over 4\n\n";
1383             }
1384            
1385             # Run through each block one at a time, documenting all of the fields in
1386             # the corresponding field list.
1387            
1388 0           my %uniq_block;
1389            
1390 0           foreach my $i (0..$#blocks)
1391             {
1392 0           my $block_name = $blocks[$i];
1393 0           my $block_label = $labels[$i];
1394            
1395             # Make sure to only process each block once, even if it is listed more
1396             # than once.
1397            
1398 0 0         next if $uniq_block{$block_name}; $uniq_block{$block_name} = 1;
  0            
1399            
1400 0           my $output_list = $ds->{block}{$block_name}{output_list};
1401 0 0         next unless ref $output_list eq 'ARRAY';
1402            
1403 0           foreach my $r (@$output_list)
1404             {
1405 0 0         next unless defined $r->{output};
1406             $doc_string .= $ds->document_field($block_label, \@vocab_list, $r, $reverse_map)
1407 0 0         unless $r->{undocumented};
1408             }
1409             }
1410            
1411 0           $doc_string .= "\n=back\n\n";
1412            
1413 0           return $doc_string;
1414             }
1415              
1416              
1417             sub document_summary {
1418              
1419 0     0 0   my ($ds, $path) = @_;
1420            
1421             # Return the empty string unless a summary block was defined for this path.
1422            
1423 0           my $summary_block = $ds->node_attr($path, 'summary');
1424 0 0         return '' unless $summary_block;
1425            
1426             # Otherwise, determine the set of vocabularies that are allowed for this
1427             # path. If none are specifically selected for this path, then all of the
1428             # vocabularies defined for this data service are allowed.
1429            
1430 0   0       my $vocabularies; $vocabularies = $ds->node_attr($path, 'allow_vocab') || $ds->{vocab};
  0            
1431            
1432 0 0 0       unless ( ref $vocabularies eq 'HASH' && keys %$vocabularies )
1433             {
1434 0           return '';
1435             }
1436            
1437             my @vocab_list = grep { $vocabularies->{$_} &&
1438             ref $ds->{vocab}{$_} &&
1439 0 0 0       ! $ds->{vocab}{$_}{disabled} } @{$ds->{vocab_list}};
  0            
  0            
1440            
1441 0 0         unless ( @vocab_list )
1442             {
1443 0           return "";
1444             }
1445            
1446             # Now generate the header for the documentation, in Pod format. We
1447             # include the special "=for wds_table_header" line to give PodParser.pm the
1448             # information it needs to generate an HTML table.
1449            
1450 0           my $doc_string = '';
1451 0           my $field_count = scalar(@vocab_list);
1452 0           my $field_string = join ' / ', @vocab_list;
1453            
1454 0 0         if ( $field_count > 1 )
1455             {
1456 0           $doc_string .= "=for wds_table_header Field name*/$field_count | Block | Description\n\n";
1457 0           $doc_string .= "=over 4\n\n";
1458 0           $doc_string .= "=item $field_string\n\n";
1459             }
1460            
1461             else
1462             {
1463 0           $doc_string .= "=for wds_table_header Field name* | Block | Description\n\n";
1464 0           $doc_string .= "=over 4\n\n";
1465             }
1466            
1467             # Now determine the summary output list.
1468            
1469 0           my $output_list = $ds->{block}{$summary_block}{output_list};
1470 0 0         return '' unless ref $output_list eq 'ARRAY';
1471            
1472 0           foreach my $r (@$output_list)
1473             {
1474 0 0         next unless defined $r->{output};
1475             $doc_string .= $ds->document_field('summary', \@vocab_list, $r, {})
1476 0 0         unless $r->{undocumented};
1477             }
1478            
1479 0           $doc_string .= "\n=back\n\n";
1480            
1481 0           return $doc_string;
1482             }
1483              
1484              
1485             sub document_field {
1486            
1487 0     0 0   my ($ds, $block_key, $vocab_list, $r, $rev_map) = @_;
1488            
1489 0           my @names;
1490            
1491 0           foreach my $v ( @$vocab_list )
1492             {
1493             my $n = defined $r->{"${v}_name"} ? $r->{"${v}_name"}
1494             : defined $r->{name} ? $r->{name}
1495             : $ds->{vocab}{$v}{use_field_names} ? $r->{output}
1496 0 0         : '';
    0          
    0          
1497            
1498 0   0       $n ||= 'I';
1499            
1500 0           push @names, $n
1501             }
1502            
1503 0           my $names = join ' / ', @names;
1504            
1505 0   0       my $descrip = $r->{doc_string} || "";
1506            
1507 0 0         if ( defined $r->{if_block} )
1508             {
1509 0 0         if ( ref $r->{if_block} eq 'ARRAY' )
1510             {
1511 0   0       $block_key = join(', ', map { $rev_map->{$_} // $_ } @{$r->{if_block}});
  0            
  0            
1512             }
1513             else
1514             {
1515 0   0       $block_key = $rev_map->{$r->{if_block}} // $r->{if_block};
1516             }
1517             }
1518            
1519 0           my $line = "\n=item $names ( $block_key )\n\n$descrip\n";
1520            
1521 0           return $line;
1522             }
1523              
1524              
1525             # process_record ( request, record, steps )
1526             #
1527             # Execute any per-record processing steps that have been defined for this
1528             # record. Return true if the record is to be included in the result, false
1529             # otherwise.
1530              
1531             sub process_record {
1532            
1533 0     0 0   my ($ds, $request, $record, $steps) = @_;
1534            
1535             # If there are no processing steps to do, return immediately.
1536            
1537 0 0 0       return 1 unless ref $steps eq 'ARRAY' and @$steps;
1538            
1539             # Otherwise go through the steps one by one.
1540            
1541 0           foreach my $p ( @$steps )
1542             {
1543             # Skip this processing step based on a conditional field value, if one
1544             # is defined.
1545            
1546 0 0         if ( my $cond_field = $p->{if_field} )
    0          
1547             {
1548 0 0         next unless defined $record->{$cond_field};
1549 0 0 0       next if ref $record->{$cond_field} eq 'ARRAY' && @{$record->{$cond_field}} == 0;
  0            
1550             }
1551            
1552             elsif ( $cond_field = $p->{not_field} )
1553             {
1554 0 0 0       next if defined $record->{$cond_field} && ref $record->{$cond_field} ne 'ARRAY';
1555 0 0 0       next if ref $record->{$cond_field} eq 'ARRAY' && @{$record->{$cond_field}} > 0;
  0            
1556             }
1557            
1558             # If this step is a 'check_field' step, then do the check.
1559            
1560 0 0         if ( defined $p->{check_field} )
    0          
1561             {
1562 0           $ds->check_field_type($record, $p->{check_field}, $p->{data_type}, $p->{subst});
1563 0           next;
1564             }
1565            
1566             # If this step is a 'check' step (i.e. check the entire record) then
1567             # do the check. If it fails, we return false.
1568            
1569             elsif ( defined $p->{check} )
1570             {
1571 0           my $check_value = $p->{check};
1572            
1573             # If the value is '*' or the empty string, then we must have a
1574             # code reference to call.
1575            
1576 0 0 0       if ( $check_value eq '' || $check_value eq '*' )
    0 0        
    0          
    0          
1577             {
1578 0           return $p->{code}($request, $record);
1579             }
1580            
1581             # Otherwise, if the value is '1' or '0' then return that. The
1582             # former will cause the record to be included in the result, the
1583             # latter will cause it to be skipped. This is mainly useful in
1584             # conjunction with 'if_field' or 'not_field'.
1585            
1586             elsif ( $check_value eq '1' || $check_value eq '0' )
1587             {
1588 0           return $check_value;
1589             }
1590            
1591             # Otherwise, we assume that we have been given a field name and
1592             # either call a code reference or do a hash-table lookup.
1593            
1594             elsif ( defined $p->{code} )
1595             {
1596 0           my $value = $record->{$check_value};
1597 0           return $p->{code}($request, $value);
1598             }
1599            
1600             elsif ( defined $p->{lookup} )
1601             {
1602 0           my $value = $record->{$check_value};
1603 0   0       return $p->{lookup}{$value} // $p->{default};
1604             }
1605            
1606             # Otherwise, we just return the value of the specified field. The
1607             # record will be included if this value is true, and skipped if
1608             # false.
1609            
1610             else
1611             {
1612 0           return $record->{$check_value};
1613             }
1614            
1615 0           next;
1616             }
1617            
1618             # If we get here, the current rule must be a 'set'. Figure out which
1619             # field (if any) we are affecting. A value of '*' means to use the
1620             # entire record (only relevant with 'code').
1621            
1622 0           my $set_field = $p->{set};
1623            
1624             # Figure out which field (if any) we are looking at. Skip this
1625             # processing step if the source field is empty, unless the attribute
1626             # 'always' is set.
1627            
1628 0   0       my $source_field = $p->{from} || $p->{from_each} || $p->{set};
1629            
1630             # Skip any processing step if the record does not have a non-empty
1631             # value in the corresponding field (unless the 'always' attribute is
1632             # set).
1633            
1634 0 0 0       if ( $source_field && $source_field ne '*' && ! $p->{always} )
      0        
1635             {
1636 0 0         next unless defined $record->{$source_field};
1637 0 0 0       next if ref $record->{$source_field} eq 'ARRAY' && @{$record->{$source_field}} == 0;
  0            
1638             }
1639            
1640             # Now generate a list of result values, according to the attributes of this
1641             # processing step.
1642            
1643 0           my @result;
1644            
1645             # If we have a 'code' attribute, then call it.
1646            
1647 0 0         if ( ref $p->{code} eq 'CODE' )
    0          
    0          
    0          
1648             {
1649 0 0         if ( $source_field eq '*' )
    0          
    0          
1650             {
1651 0           @result = $p->{code}($request, $record);
1652             }
1653            
1654             elsif ( $p->{from_each} )
1655             {
1656 0           @result = map { $p->{code}($request, $_) }
1657             (ref $record->{$source_field} eq 'ARRAY' ?
1658 0 0         @{$record->{$source_field}} : $record->{$source_field});
  0            
1659             }
1660            
1661             elsif ( $p->{from} )
1662             {
1663 0           @result = $p->{code}($request, $record->{$source_field});
1664             }
1665            
1666             else
1667             {
1668 0           @result = $p->{code}($request, $record->{$set_field});
1669             }
1670             }
1671            
1672             # If we have a 'lookup' attribute, then use it.
1673            
1674             elsif ( ref $p->{lookup} eq 'HASH' )
1675             {
1676 0 0 0       if ( $p->{from_each} )
    0          
    0          
1677             {
1678 0 0         if ( ref $record->{$source_field} eq 'ARRAY' )
    0          
1679             {
1680 0   0       @result = map { $p->{lookup}{$_} // $p->{default} } @{$record->{$source_field}};
  0            
  0            
1681             }
1682             elsif ( ! ref $record->{$source_field} )
1683             {
1684 0   0       @result = $p->{lookup}{$record->{$source_field}} // $p->{default};
1685             }
1686             }
1687            
1688             elsif ( $p->{from} )
1689             {
1690             @result = $p->{lookup}{$record->{$source_field}} // $p->{default}
1691 0 0 0       unless ref $record->{$source_field};
1692             }
1693            
1694             elsif ( $set_field ne '*' && ! ref $record->{$set_field} )
1695             {
1696 0 0 0       @result = $p->{lookup}{$record->{$set_field}} // $p->{default} if defined $record->{$set_field};
1697             }
1698             }
1699            
1700             # If we have a 'split' attribute, then use it.
1701            
1702             elsif ( defined $p->{split} )
1703             {
1704 0 0         if ( $p->{from_each} )
    0          
    0          
1705             {
1706 0 0         if ( ref $record->{$source_field} eq 'ARRAY' )
    0          
1707             {
1708 0           @result = map { split($p->{split}, $_) } @{$record->{$source_field}};
  0            
  0            
1709             }
1710             elsif ( ! ref $record->{$source_field} )
1711             {
1712 0           @result = split($p->{split}, $record->{$source_field});
1713             }
1714             }
1715            
1716             elsif ( $p->{from} )
1717             {
1718             @result = split $p->{split}, $record->{$source_field}
1719 0 0 0       if defined $record->{$source_field} && ! ref $record->{$source_field};
1720             }
1721            
1722             elsif ( $set_field ne '*' )
1723             {
1724             @result = split $p->{split}, $record->{$set_field}
1725 0 0 0       if defined $record->{$set_field} && ! ref $record->{$set_field};
1726             }
1727             }
1728            
1729             # If we have a 'join' attribute, then use it.
1730            
1731             elsif ( defined $p->{join} )
1732             {
1733 0 0         if ( $source_field )
    0          
1734             {
1735 0           @result = join($p->{join}, @{$record->{$source_field}})
1736 0 0         if ref $record->{$source_field} eq 'ARRAY';
1737             }
1738            
1739             elsif ( $set_field ne '*' )
1740             {
1741 0           @result = join($p->{join}, @{$record->{$set_field}})
1742 0 0         if ref $record->{$set_field} eq 'ARRAY';
1743             }
1744             }
1745            
1746             # Otherwise, we just use the vaoue of the source field.
1747            
1748             else
1749             {
1750             @result = ref $record->{$source_field} eq 'ARRAY' ?
1751 0 0         @{$record->{$source_field}} : $record->{$source_field};
  0            
1752             }
1753            
1754             # If the value of 'set' is '*', then we're done. This is generally
1755             # only used to call a procedure with side effects.
1756            
1757 0 0         next if $set_field eq '*';
1758            
1759             # Otherwise, use the value to modify the specified field of the record.
1760            
1761             # If the attribute 'append' is set, then append to the specified field.
1762             # Convert the value to an array if it isn't already.
1763            
1764 0 0         if ( $p->{append} )
1765             {
1766             $record->{$set_field} = [ $record->{$set_field} ] if defined $record->{$set_field}
1767 0 0 0       and ref $record->{$set_field} ne 'ARRAY';
1768            
1769 0           push @{$record->{$set_field}}, @result;
  0            
1770             }
1771            
1772             else
1773             {
1774 0 0         if ( @result == 1 )
    0          
    0          
1775             {
1776 0           ($record->{$set_field}) = @result;
1777             }
1778            
1779             elsif ( @result > 1 )
1780             {
1781 0           $record->{$set_field} = \@result;
1782             }
1783            
1784             elsif ( not $p->{always} )
1785             {
1786 0           delete $record->{$set_field};
1787             }
1788            
1789             else
1790             {
1791 0           $record->{$set_field} = '';
1792             }
1793             }
1794             }
1795            
1796 0           return 1;
1797             }
1798              
1799              
1800             # check_field_type ( record, field, type, subst )
1801             #
1802             # Make sure that the specified field matches the specified data type. If not,
1803             # substitute the specified value.
1804              
1805             sub check_field_type {
1806              
1807 0     0 0   my ($ds, $record, $field, $type, $subst) = @_;
1808            
1809 0 0         return unless defined $record->{$field};
1810            
1811 0 0         if ( $type eq 'int' )
    0          
    0          
    0          
1812             {
1813 0 0         return if $record->{$field} =~ qr< ^ -? [1-9][0-9]* $ >x;
1814             }
1815            
1816             elsif ( $type eq 'pos' )
1817             {
1818 0 0         return if $record->{$field} =~ qr< ^ [1-9][0-9]* $ >x;
1819             }
1820            
1821             elsif ( $type eq 'dec' )
1822             {
1823 0 0         return if $record->{$field} =~ qr< ^ -? (?: [1-9][0-9]* (?: \. [0-9]* )? | [0]? \. [0-9]+ | [0] \.? ) $ >x;
1824             }
1825            
1826             elsif ( $type eq 'sci' )
1827             {
1828 0 0         return if $record->{$field} =~ qr< ^ -? (?: [1-9][0-9]* \. [0-9]* | [0]? \. [0-9]+ | [0] \. ) (?: [eE] -? [1-9][0-9]* ) $ >x;
1829             }
1830            
1831             # If the data type is something we don't recognize, don't do any check.
1832            
1833             else
1834             {
1835 0           return;
1836             }
1837            
1838             # If we get here, then the value failed the test. If we were given a
1839             # replacement value, substitute it. Otherwise, just delete the field.
1840            
1841 0 0         if ( defined $subst )
1842             {
1843 0           $record->{$field} = $subst;
1844             }
1845            
1846             else
1847             {
1848 0           delete $record->{$field};
1849             }
1850             }
1851              
1852              
1853             # _generate_single_result ( request )
1854             #
1855             # This function is called after an operation is executed and returns a single
1856             # record. Return this record formatted as a single string according to the
1857             # specified output format.
1858              
1859             sub _generate_single_result {
1860              
1861 0     0     my ($ds, $request) = @_;
1862            
1863             # Determine the output format and figure out which class implements it.
1864            
1865 0           my $format = $request->output_format;
1866 0           my $format_class = $ds->{format}{$format}{package};
1867            
1868 0 0         die "could not generate a result in format '$format': no implementing module was found"
1869             unless $format_class;
1870            
1871 0           my $path = $request->node_path;
1872 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
1873            
1874             # Set the result count to 1, in case the client asked for it.
1875            
1876 0           $request->{result_count} = 1;
1877            
1878             # Get the lists that specify how to process each record and which fields
1879             # to output.
1880            
1881 0           my $proc_list = $request->{proc_list};
1882 0           my $field_list = $request->{field_list};
1883            
1884             # Make sure we have at least one field to output.
1885            
1886 0 0 0       unless ( ref $field_list && @$field_list )
1887             {
1888 0           $request->add_warning("No output fields were defined for this request.");
1889             }
1890            
1891             # If there are any processing steps to do, then do them.
1892            
1893 0           $ds->process_record($request, $request->{main_record}, $proc_list);
1894            
1895             # If there is a before_record_hook defined for this path, call it now. For a single result,
1896             # calls to 'skip_output_record' are not allowed.
1897            
1898 0 0         if ( $ds->{hook_enabled}{before_record_hook} )
1899             {
1900             $ds->_call_hooks($path, 'before_record_hook', $request, $request->{main_record})
1901 0           }
1902            
1903             # Generate the initial part of the output, before the first record.
1904            
1905 0           my $header = $format_class->emit_header($request, $field_list);
1906            
1907             # Generate the output corresponding to our single record.
1908            
1909 0           my $record = $format_class->emit_record($request, $request->{main_record}, $field_list);
1910            
1911             # Generate the final part of the output, after the last record.
1912            
1913 0           my $footer = $format_class->emit_footer($request, $field_list);
1914              
1915             # If an after_serialize_hook is defined for this path, call it.
1916            
1917 0 0         if ( $serial_hook )
1918             {
1919 0           my $rs = '';
1920            
1921 0           $ds->_call_hook_list($serial_hook, $request, 'header', \$header);
1922 0           $ds->_call_hook_list($serial_hook, $request, 'record', \$rs, \$record);
1923 0           $ds->_call_hook_list($serial_hook, $request, 'footer', \$footer);
1924             }
1925            
1926 0           return $header . $record . $footer;
1927             }
1928              
1929              
1930             # _generate_compound_result ( request )
1931             #
1932             # This function is called after an operation is executed and returns a result
1933             # set, provided that the entire result set does not need to be processed
1934             # before output. It serializes each result record according to the specified output
1935             # format and returns the resulting string. If $streaming_threshold is
1936             # specified, and if the size of the output exceeds this threshold, this
1937             # routine then sets up to stream the rest of the output.
1938              
1939             sub _generate_compound_result {
1940              
1941 0     0     my ($ds, $request, $streaming_threshold) = @_;
1942            
1943             # Determine the output format and figure out which class implements it.
1944            
1945 0           my $format = $request->output_format;
1946 0           my $format_class = $ds->{format}{$format}{package};
1947            
1948 0 0         die "could not generate a result in format '$format': no implementing module was found"
1949             unless $format_class;
1950            
1951 0           my $path = $request->node_path;
1952 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
1953            
1954             # Get the lists that specify how to process each record and which fields
1955             # to output.
1956            
1957 0           my $proc_list = $request->{proc_list};
1958 0           my $field_list = $request->{field_list};
1959            
1960             # If we have an explicit result list, then we know the count.
1961            
1962 0           $request->{result_count} = scalar(@{$request->{main_result}})
1963 0 0         if ref $request->{main_result};
1964            
1965             # Generate the initial part of the output, before the first record.
1966            
1967 0           my $output = $format_class->emit_header($request, $field_list);
1968            
1969 0 0         if ( $serial_hook )
1970             {
1971 0           $ds->_call_hook_list($serial_hook, $request, 'header', \$output);
1972             }
1973            
1974             # A record separator is emitted before every record except the first. If
1975             # this format class does not define a record separator, use the empty
1976             # string.
1977            
1978 0 0         $request->{rs} = $format_class->can('emit_separator') ?
1979             $format_class->emit_separator($request) : '';
1980            
1981 0           my $emit_rs = 0;
1982            
1983 0           $request->{actual_count} = 0;
1984            
1985             # If we have a result limit of 0, just output the header and footer and
1986             # don't bother about the records.
1987            
1988 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} eq '0' )
    0 0        
      0        
1989             {
1990 0           $request->{limit_zero} = 1;
1991             }
1992            
1993             # Otherwise, if an offset was specified and the result method didn't
1994             # handle this itself, then skip the specified number of records.
1995            
1996             elsif ( defined $request->{result_offset} && $request->{result_offset} > 0
1997             && ! $request->{offset_handled} )
1998             {
1999 0           foreach (1..$request->{result_offset})
2000             {
2001 0 0         $ds->_next_record($request) or last;
2002             }
2003             }
2004            
2005             # Now fetch and process each output record in turn. If output streaming is
2006             # available and our total output size exceeds the threshold, switch over
2007             # to streaming.
2008            
2009             RECORD:
2010 0           while ( my $record = $ds->_next_record($request) )
2011             {
2012             # If there are any processing steps to do, then process this record.
2013            
2014 0           $ds->process_record($request, $record, $proc_list);
2015            
2016             # If there is a before_record_hook defined for this path, call it now.
2017             # If it calls 'skip_output_record', then do not output this record.
2018            
2019 0 0         if ( $ds->{hook_enabled}{before_record_hook} )
2020             {
2021 0           delete $request->{_skip_record};
2022 0           $ds->_call_hooks('before_record_hook', $request, $record);
2023 0 0         next RECORD if $request->{_skip_record};
2024             }
2025            
2026             # Generate the output for this record, preceded by a record separator if
2027             # it is not the first record.
2028            
2029 0 0         my $outrs = $emit_rs ? $request->{rs} : ''; $emit_rs = 1;
  0            
2030 0           my $outrec = $format_class->emit_record($request, $record, $field_list);
2031            
2032 0 0         if ( $serial_hook )
2033             {
2034 0           $ds->_call_hook_list($serial_hook, $request, 'record', \$outrs, \$outrec);
2035             }
2036            
2037 0           $output .= $outrs . $outrec;
2038            
2039             # Keep count of the output records, and stop if we have exceeded the
2040             # limit.
2041            
2042 0           $request->{actual_count}++;
2043            
2044 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} ne 'all' )
2045             {
2046 0 0         last if $request->{actual_count} >= $request->{result_limit};
2047             }
2048            
2049             # If streaming is a possibility, check whether we have passed the
2050             # threshold for result size. If so, then we need to immediately
2051             # stash the output generated so far and call stream_data. Doing that
2052             # will cause the current function to be aborted, followed by an
2053             # automatic call to &stream_result (defined below).
2054            
2055 0 0 0       if ( defined $streaming_threshold && length($output) > $streaming_threshold )
2056             {
2057 0           $request->{stashed_output} = $output;
2058 0           Dancer::Plugin::StreamData::stream_data($request, &_stream_compound_result);
2059             }
2060             }
2061            
2062             # If we get here, then we did not initiate streaming. So add the
2063             # footer and return the output data.
2064            
2065             # If we didn't output any records, give the formatter a chance to indicate
2066             # this.
2067            
2068 0 0         unless ( $request->{actual_count} )
2069             {
2070 0           my $empty = $format_class->emit_empty($request);
2071            
2072 0 0         if ( $serial_hook )
2073             {
2074 0           $ds->_call_hook_list($serial_hook, $request, 'empty', \$empty);
2075             }
2076              
2077 0           $output .= $empty;
2078             }
2079            
2080             # Generate the final part of the output, after the last record.
2081            
2082 0           my $footer = $format_class->emit_footer($request, $field_list);
2083              
2084 0 0         if ( $serial_hook )
2085             {
2086 0           $ds->_call_hook_list($serial_hook, $request, 'footer', \$footer);
2087             }
2088            
2089 0           $output .= $footer;
2090            
2091             # Determine if we need to encode the output into the proper character set.
2092             # Usually Dancer does this for us, but only if it recognizes the content
2093             # type as text. For these formats, the definition should set the
2094             # attribute 'encode_as_text' to true.
2095            
2096 0           my $output_charset = $ds->{_config}{charset};
2097 0           my $must_encode;
2098            
2099 0 0 0       if ( $output_charset
      0        
2100             && $ds->{format}{$format}{encode_as_text}
2101             && ! $request->{content_type_is_text} )
2102             {
2103 0           $must_encode = 1;
2104             }
2105            
2106 0 0         return $must_encode ? encode($output_charset, $output) : $output;
2107             }
2108              
2109              
2110             # _generate_processed_result ( request )
2111             #
2112             # This function is called if the result set needs to be processed in its
2113             # entirety before being output. It processes the entire result set and
2114             # collects a list of processed records, and then serializes each result record
2115             # according to the specified output format. If $streaming_threshold is
2116             # specified, and if the size of the output exceeds this threshold, this
2117             # routine then sets up to stream the rest of the output.
2118              
2119             sub _generate_processed_result {
2120              
2121 0     0     my ($ds, $request, $streaming_threshold) = @_;
2122            
2123             # Determine the output format and figure out which class implements it.
2124            
2125 0           my $format = $request->output_format;
2126 0           my $format_class = $ds->{format}{$format}{package};
2127            
2128 0 0         die "could not generate a result in format '$format': no implementing module was found"
2129             unless $format_class;
2130            
2131 0           $ds->debug_line("Processing result set before output.");
2132            
2133 0           my $path = $request->node_path;
2134 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
2135            
2136             # Get the lists that specify how to process each record and which fields
2137             # to output.
2138            
2139 0           my $proc_list = $request->{proc_list};
2140 0           my $field_list = $request->{field_list};
2141            
2142             # Now fetch and process each output record in turn. Collect up all of the
2143             # records that pass the processing phase in a list.
2144            
2145 0           my @results;
2146            
2147             RECORD:
2148 0           while ( my $record = $ds->_next_record($request) )
2149             {
2150             # If there are any processing steps to do, then process this record.
2151             # If the return value is not true, skip the record.
2152            
2153 0 0         $ds->process_record($request, $record, $proc_list) or next RECORD;
2154            
2155             # If there is a before_record_hook defined for this path, call it now.
2156             # If it calls 'skip_output_record', then do not output this record.
2157            
2158 0 0         if ( $ds->{hook_enabled}{before_record_hook} )
2159             {
2160 0           delete $request->{_skip_record};
2161 0           $ds->_call_hooks('before_record_hook', $request, $record);
2162 0 0         next RECORD if $request->{_skip_record};
2163             }
2164            
2165             # Add the record to the list.
2166            
2167 0           push @results, $record;
2168             }
2169            
2170             # We now know the result count.
2171            
2172 0           $request->{result_count} = scalar(@results);
2173            
2174             # At this point, we can generate the output. We start with the header.
2175            
2176 0           my $output = $format_class->emit_header($request, $field_list);
2177            
2178 0 0         if ( $serial_hook )
2179             {
2180 0           $ds->_call_hook_list($serial_hook, $request, 'header', \$output);
2181             }
2182            
2183             # A record separator is emitted before every record except the first. If
2184             # this format class does not define a record separator, use the empty
2185             # string.
2186            
2187 0 0         $request->{rs} = $format_class->can('emit_separator') ?
2188             $format_class->emit_separator($request) : '';
2189            
2190 0           my $emit_rs = 0;
2191            
2192 0           $request->{actual_count} = 0;
2193            
2194             # If an offset was specified and the result method didn't handle this
2195             # itself, then skip the specified number of records.
2196            
2197 0 0 0       if ( defined $request->{result_offset} && $request->{result_offset} > 0
      0        
2198             && ! $request->{offset_handled} )
2199             {
2200 0           splice(@results, 0, $request->{result_offset});
2201             }
2202            
2203             # If the result limit is zero, we can ignore all records.
2204            
2205 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} eq '0' )
2206             {
2207 0           @results = ();
2208             }
2209            
2210             # Otherwise iterate over all of the remaining records.
2211            
2212             OUTPUT:
2213 0           while ( @results )
2214             {
2215 0           my $record = shift @results;
2216            
2217             # Generate the output for this record, preceded by a record separator if
2218             # it is not the first record.
2219            
2220 0 0         my $outrs = $emit_rs ? $request->{rs} : ''; $emit_rs = 1;
  0            
2221 0           my $outrec = $format_class->emit_record($request, $record, $field_list);
2222            
2223 0 0         if ( $serial_hook )
2224             {
2225 0           $ds->_call_hook_list($serial_hook, $request, 'record', \$outrs, \$outrec);
2226             }
2227            
2228 0           $output .= $outrs . $outrec;
2229            
2230             # Keep count of the output records, and stop if we have exceeded the
2231             # limit.
2232            
2233 0           $request->{actual_count}++;
2234            
2235 0 0 0       if ( defined $request->{result_limit} && $request->{result_limit} ne 'all' )
2236             {
2237 0 0         last if $request->{actual_count} >= $request->{result_limit};
2238             }
2239            
2240             # If streaming is a possibility, check whether we have passed the
2241             # threshold for result size. If so, then we need to immediately
2242             # stash the output generated so far and call stream_data. Doing that
2243             # will cause the current function to be aborted, followed by an
2244             # automatic call to &stream_result (defined below).
2245            
2246 0 0 0       if ( defined $streaming_threshold && length($output) > $streaming_threshold )
2247             {
2248 0           $request->{stashed_output} = $output;
2249 0           $request->{stashed_results} = \@results;
2250 0           $request->{processing_complete} = 1;
2251 0           Dancer::Plugin::StreamData::stream_data($request, &_stream_compound_result);
2252             }
2253             }
2254            
2255             # If we get here, then we did not initiate streaming. So add the
2256             # footer and return the output data.
2257            
2258             # If we didn't output any records, give the formatter a chance to indicate
2259             # this.
2260            
2261 0 0         unless ( $request->{actual_count} )
2262             {
2263 0           my $empty = $format_class->emit_empty($request);
2264            
2265 0 0         if ( $serial_hook )
2266             {
2267 0           $ds->_call_hook_list($serial_hook, $request, 'empty', \$empty);
2268             }
2269              
2270 0           $output .= $empty;
2271             }
2272            
2273             # Generate the final part of the output, after the last record.
2274            
2275 0           my $footer = $format_class->emit_footer($request, $field_list);
2276            
2277 0 0         if ( $serial_hook )
2278             {
2279 0           $ds->_call_hook_list($serial_hook, $request, 'footer', \$footer);
2280             }
2281            
2282 0           $output .= $footer;
2283            
2284             # Determine if we need to encode the output into the proper character set.
2285             # Usually Dancer does this for us, but only if it recognizes the content
2286             # type as text. For these formats, the definition should set the
2287             # attribute 'encode_as_text' to true.
2288            
2289 0           my $output_charset = $ds->{_config}{charset};
2290 0           my $must_encode;
2291            
2292 0 0 0       if ( $output_charset
      0        
2293             && $ds->{format}{$format}{encode_as_text}
2294             && ! $request->{content_type_is_text} )
2295             {
2296 0           $must_encode = 1;
2297             }
2298            
2299 0 0         return $must_encode ? encode($output_charset, $output) : $output;
2300             }
2301              
2302              
2303             # _stream_compound_result ( )
2304             #
2305             # Continue to generate a compound query result from where
2306             # generate_compound_result() left off, and stream it to the client
2307             # record-by-record.
2308             #
2309             # This routine must be passed a Plack 'writer' object, to which will be
2310             # written in turn the stashed output from generate_compound_result(), each
2311             # subsequent record, and then the footer. Each of these chunks of data will
2312             # be immediately sent off to the client, instead of being marshalled together
2313             # in memory. This allows the server to send results up to hundreds of
2314             # megabytes in length without bogging down.
2315              
2316             sub _stream_compound_result {
2317            
2318 0     0     my ($request, $writer) = @_;
2319            
2320 0           my $ds = $request->{ds};
2321            
2322             # Determine the output format and figure out which class implements it.
2323            
2324 0           my $format = $request->output_format;
2325 0           my $format_class = $ds->{format}{$format}{package};
2326 0           my $format_is_text = $ds->{format}{$format}{is_text};
2327            
2328 0 0         croak "could not generate a result in format '$format': no implementing class"
2329             unless $format_class;
2330            
2331 0           my $path = $request->node_path;
2332 0   0       my $serial_hook = $ds->{hook_enabled}{after_serialize_hook} && $ds->node_attr($path, 'after_serialize_hook');
2333            
2334             # Get the lists that specify how to process each record and which fields
2335             # to output.
2336            
2337 0           my $proc_list = $request->{proc_list};
2338 0           my $field_list = $request->{field_list};
2339            
2340             # Determine the output character set, because we will need to encode text
2341             # responses in it.
2342            
2343 0           my $output_charset = $ds->{_config}{charset};
2344            
2345             #return $must_encode ? encode($output_charset, $output) : $output;
2346            
2347             # First send out the partial output previously stashed by
2348             # generate_compound_result().
2349            
2350 0 0 0       if ( $output_charset && $format_is_text )
2351             {
2352 0           $writer->write( encode($output_charset, $ds->{stashed_output}) );
2353             }
2354            
2355             else
2356             {
2357 0           $writer->write( $ds->{stashed_output} );
2358             }
2359            
2360             # Then process the remaining rows.
2361            
2362             RECORD:
2363 0           while ( my $record = $ds->_next_record($request) )
2364             {
2365             # If there are any processing steps to do, then process this record. But skip this if this
2366             # subroutine was called from '_generate_processed_result'.
2367              
2368 0 0         unless ( $request->{processing_complete} )
2369             {
2370 0           $ds->process_record($request, $record, $proc_list);
2371            
2372             # If there is a before_record_hook defined for this path, call it now.
2373             # If it calls 'skip_output_record', then do not output this record.
2374            
2375 0 0         if ( $ds->{hook_enabled}{before_record_hook} )
2376             {
2377 0           delete $request->{_skip_record};
2378 0           $ds->_call_hooks('before_record_hook', $request, $record);
2379 0 0         next RECORD if $request->{_skip_record};
2380             }
2381             }
2382            
2383             # Generate the output for this record, preceded by a record separator if
2384             # it is not the first record.
2385            
2386 0           my $outrs = $request->{rs};
2387 0           my $outrec = $format_class->emit_record($request, $record, $field_list);
2388            
2389 0 0         if ( $serial_hook )
2390             {
2391 0           $ds->_call_hook_list($serial_hook, $request, 'record', \$outrs, \$outrec);
2392             }
2393            
2394 0           my $output .= $outrs . $outrec;
2395            
2396 0 0 0       if ( ! defined $output or $output eq '' )
    0 0        
2397             {
2398             # do nothing
2399             }
2400            
2401             elsif ( $output_charset && $format_is_text )
2402             {
2403 0           $writer->write( encode($output_charset, $output) );
2404             }
2405            
2406             else
2407             {
2408 0           $writer->write( $output );
2409             }
2410            
2411             # Keep count of the output records, and stop if we have exceeded the
2412             # limit.
2413            
2414             last if $request->{result_limit} ne 'all' &&
2415 0 0 0       ++$request->{actual_count} >= $request->{result_limit};
2416             }
2417            
2418             # finish output...
2419            
2420             # my $final = $ds->finishOutput();
2421             # $writer->write( encode_utf8($final) ) if defined $final and $final ne '';
2422            
2423             # Finally, send out the footer and then close the writer object.
2424            
2425             # Generate the final part of the output, after the last record.
2426            
2427 0           my $footer = $format_class->emit_footer($request, $field_list);
2428            
2429 0 0         if ( $serial_hook )
2430             {
2431 0           $ds->_call_hook_list($serial_hook, $request, 'footer', \$footer);
2432             }
2433            
2434 0 0 0       if ( ! defined $footer or $footer eq '' )
    0 0        
2435             {
2436             # do nothing
2437             }
2438            
2439             elsif ( $output_charset && $format_is_text )
2440             {
2441 0           $writer->write( encode($output_charset, $footer) );
2442             }
2443            
2444             else
2445             {
2446 0           $writer->write( $footer );
2447             }
2448            
2449 0           $writer->close();
2450             }
2451              
2452              
2453             # _next_record ( request )
2454             #
2455             # Return the next record to be output for the given request. If
2456             # $ds->{main_result} is set, use that first. Once that is exhausted (or if
2457             # it was never set) then if $result->{main_sth} is set then read records from
2458             # it until exhausted.
2459              
2460             sub _next_record {
2461            
2462 0     0     my ($ds, $request) = @_;
2463            
2464             # If the request has a zero limit, and no processing needs to be done on
2465             # the result set, then no records need to be returned.
2466            
2467 0 0         return if $request->{limit_zero};
2468            
2469             # If we have a stashed result list, return the next item in it.
2470            
2471 0 0 0       if ( ref $request->{stashed_results} eq 'ARRAY' )
    0          
    0          
2472             {
2473 0           return shift @{$request->{stashed_results}};
  0            
2474             }
2475            
2476             # If we have a 'main_result' array with something in it, return the next
2477             # item in it.
2478            
2479 0           elsif ( ref $request->{main_result} eq 'ARRAY' and @{$request->{main_result}} )
2480             {
2481 0           return shift @{$request->{main_result}};
  0            
2482             }
2483            
2484             # Otherwise, if we have a 'main_sth' statement handle, read the next item
2485             # from it.
2486            
2487             elsif ( ref $request->{main_sth} )
2488             {
2489             return $request->{main_sth}->fetchrow_hashref
2490 0           }
2491            
2492             else
2493             {
2494 0           return;
2495             }
2496             }
2497              
2498              
2499             # _generate_empty_result ( request )
2500             #
2501             # This function is called after an operation is executed and returns no results
2502             # at all. Return the header and footer only.
2503              
2504             sub _generate_empty_result {
2505            
2506 0     0     my ($ds, $request) = @_;
2507            
2508             # Determine the output format and figure out which class implements it.
2509            
2510 0           my $format = $request->output_format;
2511 0           my $format_class = $ds->{format}{$format}{package};
2512            
2513 0 0         croak "could not generate a result in format '$format': no implementing class"
2514             unless $format_class;
2515            
2516             # Call the appropriate methods from this class to generate the header,
2517             # and footer.
2518            
2519 0           my $output = $format_class->emit_header($request);
2520            
2521 0           $output .= $format_class->emit_empty($request);
2522 0           $output .= $format_class->emit_footer($request);
2523            
2524 0           return $output;
2525             }
2526              
2527              
2528             1;