#! /usr/bin/perl -T

# Read a database table, present its records in a HTML table.

my $dir;
BEGIN {
    use Cwd qw(abs_path);
    $dir = abs_path(__FILE__);
    $dir =~ /(.*)(\/.*\/.*\/.*\..*)$/;
    $dir = $1;
    unshift(@INC, $dir . "/lib");
}

use strict;
use warnings;
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Authen::OATH;
use CGI qw(:standard -nosticky -utf8 start_div end_div start_span end_span);
use Encode qw(decode);
use Excel::Writer::XLSX;
use File::Basename qw(basename dirname);
use File::Temp;
use HTML::Entities;
use Hash::Merge qw(merge);
use List::MoreUtils qw(uniq);
use Scalar::Util qw(blessed);
use Spreadsheet::Wright::Excel;
use Spreadsheet::Wright::OpenDocument;
use URI::Encode qw( uri_encode uri_decode );
use JSON qw(encode_json);

use CGIParameters qw(read_cgi_parameters);
use Database;
use Database::Order;
use Database::Filter;
use HTMLGenerator qw(
    data2html_list_table
    data2html_new_form
    error_page
    forward_backward_control_form
    start_html5
    transfer_query_string
    nameless_submit
    start_form_QS
    start_root
    end_root
    header_db_pl
    start_doc_wrapper
    end_doc_wrapper
    start_main_content
    end_main_content
    footer_db_pl
    start_card_fluid
    end_card
    nameless_submit_disabled
    three_column_row_html
    two_column_row_html
    button_classes_string
    warnings2html
    datalists
);
use RestfulDB::CGI qw( save_cgi_parameters );
use RestfulDB::Defaults qw( get_css_styles get_default_cgi_parameters );
use RestfulDB::DBSettings qw( get_database_settings );
use RestfulDB::Exception;
use RestfulDB::JSON qw( data2json error2json json2data );
use RestfulDB::JSONAPI qw( data2collection error2jsonapi jsonapi2data );
use RestfulDB::Spreadsheet qw( data2spreadsheet data2template spreadsheet2data );

# STDIN must NOT be set to binmode "UTF-8", since CGI with the '-utf8'
# flag handles this.
binmode( STDOUT, "utf8" );
binmode( STDERR, "utf8" );

my $page_level = 1;

$ENV{PATH} = ""; # Untaint and clean PATH

# Path to database directory for SQlite3 databases:
my $db_dir = $dir . "/db";

Hash::Merge::set_behavior( 'RIGHT_PRECEDENT' );

my @warnings;
local $SIG{__WARN__} = sub { print STDERR $_[0]; push @warnings, $_[0] };

my $cgi = new CGI;
my $format = 'html';

# Detecting JSON/JSONAPI requests
my( $accept ) = Accept();
if(      (content_type() && content_type() eq $RestfulDB::JSONAPI::MEDIA_TYPE) ||
         ($accept && $accept eq $RestfulDB::JSONAPI::MEDIA_TYPE) ) {
    $format = 'jsonapi';
} elsif( (content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE) ||
         ($accept && $accept eq $RestfulDB::JSON::MEDIA_TYPE) ) {
    $format = 'json';
}

eval {

    my $filename_re = '[^\x{00}-\x{08}\x{0A}-\x{1F}\x{7F}]+';

    my %local_CGI_parameters = (
        select_column       => { re => '[\w\d_]+' },
        select_operator     => { re => '[\w]+' },
        select_combining    => { re => 'new|append|within' },
        select_not_operator => { re => 'not|' },
        search_value        => { re => '.*', default => '' }, # Should be SQL-escaped by the DBI layer
        filter              => { re => '.*' }, # processed afterwards

        columns         => { re => '([\w\d_]+\.?)(,[\w\d_]+\.?)*', default => '' },
        columns_add     => { re => '([\w\d_]+)(,[\w\d_]+)*', default => '' },
        columns_remove  => { re => '([\w\d_]+)(,[\w\d_]+)*', default => '' },
        depth           => { re => '-1|[0-9]+|auto', default => 0 },
        format          => { re => 'html|csv|csv[ \+]?zip|ods|xlsx?|xml|cif|json(?:api)?' },
        rows_template   => { re => '[0-9]+' },
        upload_format   => { re => 'autodetect|csv|csv[ \+]?zip|ods|xlsx?' },
        include         => { re => '.*' }, # Reserved for JSONAPI use
        action          => { re => '.*' },

        autodetectfile  => { re => $filename_re },
        csvfile         => { re => $filename_re },
        odsfile         => { re => $filename_re },
        xlsfile         => { re => $filename_re },
        xlsxfile        => { re => $filename_re },

        csvzipfile  => { re => $filename_re },
        odszipfile  => { re => $filename_re },
        xlszipfile  => { re => $filename_re },
        xlsxzipfile => { re => $filename_re },

        spreadsheet => { re => $filename_re },

        jsonfile => { re => $filename_re },

        token => { re => '[0-9]{6}' },
    );

    my( $base_uri, $query_string ) = split /\?/, $ENV{REQUEST_URI};

    my( $params, $changed );
    eval {
        ( $params, $changed ) =
            read_cgi_parameters( $cgi,
                                { %RestfulDB::Defaults::CGI_parameters,
                                  %local_CGI_parameters },
                                { passthrough_re => qr/^column:/,
                                  query_string   => $query_string } );
    };
    InputException->throw( $@ ) if $@;

    my %params = %$params;

    if( $params{debug} && $params{debug} eq 'save' ) {
        save_cgi_parameters( $db_dir );
    }

    if( $params{format} ) {
        $params{format} =~ s/ //g;
        $params{format} =~ s/\+//g;
    }

    # Assigns 'depth=auto' values according to GUI defaults.
    if( defined $params{depth} && $params{depth} eq 'auto' ) {
        if( $params{format} && $params{format} eq 'csv' ) {
            $params{depth} = 0;
        } else {
            $params{depth} = -1;
        }
    }

    if( $params{format} && $params{format} eq 'csv' &&
        defined $params{depth} && $params{depth} ne 0 ) {
        InputException->throw( "'depth' parameter has to be '0' for CSV format file." );
    }

    # Deals with the upload file handling.
    if( defined $params{action} && $params{action} eq 'upload' ) {
        if( defined $params{spreadsheet} ) {
            my $filetype = $params{upload_format};
            $filetype =~ s/\s//g;
            $filetype = $params{upload_format} . 'file';
            $params{$filetype} = $params{spreadsheet};
            $cgi->{param}{$filetype} = [ $cgi->multi_param('spreadsheet') ];
            $cgi->delete( 'spreadsheet' );
            push @{ $cgi->{'.parameters'} }, $filetype;
        } else {
            warn "no data file was passed to the upload handler\n";
        }

        delete $params{action};
        delete $params{format};
        delete $params{upload_format};
        delete $params{spreadsheet};
    }

    $format = $params{format} if $changed->{format};

    my $filter = Database::Filter->new( { filter => $params->{filter},
                                          select_column => $params->{select_column},
                                          select_operator => $params->{select_operator},
                                          search_value => $params->{search_value},
                                          select_not_operator => $params->{select_not_operator},
                                          select_combining => $params->{select_combining},
                                        } );

    # Always use *only* the filter expression for real searches; redirect
    # to the '?filter=...' URI for real search:

    if( $filter && $params{select_column} && $params{select_operator} ) {
        my $new_offset = 0;
        my $filter_qs = $filter->query_string_uri;

        my $redirect_uri = transfer_query_string(
            $base_uri, $ENV{REQUEST_URI},
            { exclude_re => 'select_.*|search_value|.cgifields|filter|rows|offset',
               append => "offset=$new_offset" .
               ( defined $params{rows} ? "&rows=$params{rows}" : '') .
               ( $filter_qs ? '&' . $filter_qs : '' )
             }
            );

        print $cgi->redirect( $redirect_uri );
        exit(0);
    }

    # Since we have transferred all search values to a querry string, the
    # orginal search values are no longer necessary and must be removed
    # from the CGI parameter lists to avoid duplication:

    for my $ref (\%params, $params, $changed) {
        delete $ref->{search_value};
        delete $ref->{select_column};
        delete $ref->{select_combining};
        delete $ref->{select_not_operator};
        delete $ref->{select_operator};
    }

    my %db_settings = get_database_settings( \%params, \%ENV,
                                             { db_dir => $db_dir, level => 1 });

    my $db_user = $db_settings{db_user};
    my $db_name = $db_settings{db_name};
    my $db_path = $db_settings{db_path};
    my $db_table = $db_settings{db_table};
    my $db_engine = $db_settings{db_engine};

    my $remote_user = $db_settings{remote_user_for_print};

    my $db_settings = {
        content_db => { DB => $db_path,
                        engine => $db_engine,
                        user => $db_user },
    };

    # Parameter 'include' is reserved for JSONAPI, and MUST trigger
    # 400 Bad Request error if requested but not implemented:
    if( $params->{include} && $format eq 'jsonapi' ) {
        InputException->throw( "'include' parameter is not supported yet" );
    }

    my $db = Database->new( $db_settings );
    $db->connect();

    if( !$changed->{id_column} ) {
        $params{id_column} = $db->get_id_column( $db_table );
    }

    my @columns;
    if( $params{columns} ) {
        # If columns are user-selected, nonexistent columns must be
        # thrown out.
        my @columns_all = $db->get_column_names( $db_table, { display => 'all' } );

        for my $column_re (split ',', $params{columns}) {
            my $column = $column_re;
            my @matching;
            if( $column =~ s/\.$// ) {
                @matching = grep { /^\Q$column\E$/ } @columns_all;
            } else {
                @matching = grep { /^\Q$column\E/  } @columns_all;
            }
            push @columns, @matching;

            if( !@matching ) {
                warn "column definition '$column' does not match any columns " .
                     "in table '$db_table'\n";
            }
        }
        @columns = uniq @columns;
    } else {
        # If columns are not user-selected, the list of all existing
        # columns of the table (except UUID and other "service"
        # columns) are taken from the database.  No columns should be
        # hidden from the user if explicitly asked for.
        @columns = $db->get_column_names( $db_table );
    }

    # Adding/removing columns from the list of shown columns by the request
    # of the user. This is handled using HTTP redirect, as is done with
    # filters.
    if( $params{columns_add} || $params{columns_remove} ) {
        $params{columns_add}    = '' unless $params{columns_add};
        $params{columns_remove} = '' unless $params{columns_remove};

        my %columns =
            map { $_ => 1 }
                $db->get_column_names( $db_table, { display => 'all' } );

        my @columns_add    = split ',', $params{columns_add};
        my @columns_remove = split ',', $params{columns_remove};

        my @nonexistent = sort grep { !$columns{$_} }
                                    ( @columns_add, @columns_remove );
        if( @nonexistent ) {
            local $" = "', '";
            warn "column(s) '@nonexistent' are not found in table '$db_table'\n";
        }

        @columns_add    = grep { $columns{$_} } @columns_add;
        @columns_remove = grep { $columns{$_} } @columns_remove;

        my %columns_remove = map { $_ => 1 } @columns_remove;
        @columns = ( ( grep { !$columns_remove{$_} } @columns ),
                     @columns_add );

        local $" = ',';
        my $redirect_uri = transfer_query_string(
            $base_uri, $ENV{REQUEST_URI},
            { exclude_re => 'columns_add|columns_remove|columns',
               append => "columns=@columns" }
            );

        print $cgi->redirect( $redirect_uri );
        exit(0);
    }

    if( $params{action} && $params{action} eq 'template' ) {
        if( !$format || $format eq 'html' ) {
            my $data =
                $db->get_record_descriptions( $db_table,
                                              {
                                                  id_column => $params{id_column},
                                                  show_fk_values => 1,
                                                  show_enumeration_values => 1,
                                                  template => 1,
                                              } );

            # Collect record data preprint html.
            # Variables set to split to page elements.
            my ( $table_html,
                 $head_html_w_navigation_buttons,
                 $record_modification_buttons );

            my $save_button =
                submit( -name => 'Save',
                        -value => 'Save',
                        -class => button_classes_string(),
                        -onclick => 'clearView(); return validate_and_submit( this.form )' );
            
            my $fk_columns  = $db->get_foreign_keys($db_table);
            my $fk_table_data = $db->get_fks_cell_text( $fk_columns, 1 );
            my $ambiguous_fks = {};

            foreach my $fk( sort keys %{ $fk_table_data->{fk_values} } ){
                my $fk_table_values = $fk_table_data->{fk_values}{$fk};
                $ambiguous_fks->{ $fk } =
                HTMLGenerator::get_ambiguous_fk_values($fk_table_values, $fk, $fk_columns);
            }

            my $new_record_html =
                data2html_new_form(
                        $db, $data, $db_table,
                        {
                            request_uri => $ENV{REQUEST_URI},
                            post_action => transfer_query_string( $base_uri,
                                                                  $ENV{REQUEST_URI},
                                                                  {
                                                                    exclude_re => 'action'
                                                                  } ),
                            level => 1,
                            vertical => 1,
                            no_add_button => 1,
                            table_properties =>
                            {
                                -class => 'record-hoverable dbdata-input-form',
                            },
                            form_head => $save_button,
                            form_tail => br . $save_button,
                            ambiguous_fks => $ambiguous_fks
            } );

            # Collect html variables set to print.
            my $column_left_html;
            my $column_right_html;
            my $selection_filter_string = '';

            # Acknowledge the user name:
            if( defined $remote_user ) {
                # 'doc' class for paragraphs ensures that their content
                # can be center-aligned.
                $column_right_html =
                    p( {-class => "doc"}, "User: $remote_user" ) . "\n";
            }
            unless( defined $new_record_html ) {
                # Hold three grid column structure for record pagination:
                # prev, next buttons and filter selection text.
                # See HTMLGenerator function.
                $column_left_html = $head_html_w_navigation_buttons;
            }

            if( $ENV{REQUEST_URI} =~ /(\?.*)$/ ) {
                $selection_filter_string = $1;
            }

            # Organize preprint element into the main html variable that's
            # gonna be printed.
            my $html = $cgi->header( -type => 'text/html',
                                     -expires => 'now',
                                     -charset => 'UTF-8' ) .
                       start_html5( $cgi,
                                    { -style => get_css_styles( $db_name, $page_level ),
                                      -head => Link({
                                                   -rel  => 'icon',
                                                   -type => 'image/x-icon',
                                                   -href => '../images/favicon.ico'
                                      }),
                                      -meta => {
                                        'viewport' => 'width=device-width, initial-scale=1',
                                      },
                                      -script => [
                                          { -type => 'text/javascript',
                                            -src => '../js/form_control.js' },
                                          { -type => 'text/javascript',
                                            -src => '../js/form_validation.js' },
                                          { -type => 'text/javascript',
                                            -src  => '../js/jquery.js' },
                                          { -type => 'text/javascript',
                                            -src  => '../js/esc_key_press.js' },
                                          { -type => 'text/javascript',
                                            -src  => '../js/select2.js' },
                                          { -type => 'text/javascript',
                                            -src  => '../js/dropdown.js' },
                                          { -type => 'text/javascript',
                                            -src  => '../js/scroll_into_view.js' },
                                          'window.onload = unhide_form_buttons' ],
                                      -title => "new record - $db_table table - $db_name db: " .
                                                $RestfulDB::Defaults::website_title } );

            # The beginning of the HTML page.
            $html .= start_root();
            $html .= header_db_pl( $db_name, $db_table );
            $html .= start_doc_wrapper();
            $html .= start_main_content();

            # Hidden 'top' paragraph on the data page for the 'To top' link.
            $html .= p( {-id => "top"}, 'Top' ) . "\n";

            # Warnings
            $html .= warnings2html( @warnings );

            # Start record navigation card.
            $html .= start_card_fluid( {class => "record-navigation-panel"} );
            # 'scale' parameter meaning : signifies the column dimension class for the
            #                             MINOR column.
            #                             So the major column fills up all of the remaining
            #                             row space.
            $html .= two_column_row_html({
                            column_major => "Record: new",
                            row_class => "page-title"
                        });

            $html .= two_column_row_html({
                            column_major => $column_right_html,
                            column_minor => $column_left_html,
                            scale_lg => 6,
                            scale_md => 12
                        });
            $html .= end_card(); # Close the first card.

            # Start the main data card.
            $html .= start_card_fluid();
            $html .= start_div( {-class => "section"} ) . "\n";
            if( defined $new_record_html ) {
                $html .= $new_record_html;
            } else {
                $html .= $record_modification_buttons;
                $html .= $table_html;
            }
            $html .= end_div() . "\n"; # Close the first section.
            $html .= end_card(); # Close second card.

            # This ends the HTML document.
            $html .= footer_db_pl( $db_name, $db_table );
            $html .= end_main_content();
            $html .= end_doc_wrapper();
            $html .= end_root();
            $html .= end_html . "\n";

            print $html;
        } elsif( $format eq 'csv' || $format eq 'csvzip' ||
                 $format eq 'ods' || $format eq 'xls' ||
                 $format eq 'xlsx' ) {
            my $requested_tables;
            if( exists $params->{include} ) {
                @$requested_tables = split ',', $params->{include};
            }
            my $data = $db->get_record_description( $db_table,
                                                    { template => 1 } );
            my $seen_vars = $cgi->Vars;
            my $options = {
                rows    => ( $seen_vars->{rows_template} ?
                             $seen_vars->{rows_template} : 0 ),
                columns => ( $seen_vars->{columns} ?
                             \@columns : undef ),
            };
            if( $format eq 'csv' ) {
                print $cgi->header( -type => 'text/csv',
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table-template.$format" );
                my $template = '';
                data2template( \$template, $data, $options );
                print $template;
            } elsif( $format eq 'csvzip' ) {
                print $cgi->header( -type => 'application/zip',
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table-template.zip" );
                my $zip = Archive::Zip->new();
                data2template( $zip, $data, $options );
                $zip->writeToFileHandle( *STDOUT, 0 );
            } elsif( $format eq 'ods' || $format eq 'xls' ) {
                my $file = File::Temp->new;
                my( $template, $type );
                if( $format eq 'ods' ) {
                    $template = Spreadsheet::Wright::OpenDocument->new( file   => $file,
                                                                        format => $format,
                                                                        sheet  => $db_table );
                    $type = 'application/vnd.oasis.opendocument.spreadsheet';
                } else {
                    $template = Spreadsheet::Wright::Excel->new( file   => $file,
                                                                 format => $format,
                                                                 sheet  => $db_table );
                    $type = 'application/vnd.ms-excel';
                }
                # A hack to store table names in Spreadsheet::Wright object,
                # as there is no way to get the list of sheets as of 0.107.
                $template->{_RESTFULDB_SHEET_NAMES} = [];
                eval {
                    data2template( $template, $data, $options );
                };
                NotImplementedException->throw( $@ ) if $@ && $@ =~ /must be <= 31 chars/;
                $template->close;
                binmode( STDOUT );
                print $cgi->header( -type => $type,
                                    -expires => 'now',
                                    -attachment => "$db_table-template.$format" );
                open( my $inp, $file );
                while( <$inp> ) {
                    print;
                }
                close $inp;
            } elsif( $format eq 'xlsx' ) {
                open my $out, '>', \my $output;
                my $template = Excel::Writer::XLSX->new( $out );
                eval {
                    data2template( $template, $data, $options );
                };
                NotImplementedException->throw( $@ ) if $@ && $@ =~ /must be <= 31 chars/;
                $template->close;

                binmode( STDOUT );
                print $cgi->header( -type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                                    -expires => 'now',
                                    -attachment => "$db_table-template.$format" ),
                      $output;
            }
        } else {
            InputException->throw( "Unsupported format '$format'" );
        }
    } else {
        if( uc($ENV{REQUEST_METHOD}) eq 'PATCH' ||
            uc($ENV{REQUEST_METHOD}) eq 'POST' ||
            uc($ENV{REQUEST_METHOD}) eq 'PUT' ) {
            if( !defined $remote_user ) {
                UnauthorizedException->throw(
                    'Editing a database requires authentication' );
            }

            my $default_action =
                uc($ENV{REQUEST_METHOD}) ne 'PATCH' ? 'insert' : 'update';

            my $data =
                $db->get_record_descriptions( $db_table,
                                              {
                                                  id_column => $params{id_column},
                                                  show_fk_values => 1,
                                                  show_enumeration_values => 1,
                                                  template => 1,
                                              } );

            my $record_data;
            my %options;
            if( defined $params{csvfile}     || defined $params{xlsfile}        ||
                defined $params{xlsxfile}    || defined $params{odsfile}        ||
                defined $params{spreadsheet} || defined $params{csvzipfile}     ||
                defined $params{xlszipfile}  || defined $params{xlsxzipfile}    ||
                defined $params{odszipfile}  || defined $params{autodetectfile} ||
                (content_type() && exists $RestfulDB::Spreadsheet::content_types{content_type()}) ) {
                $record_data = spreadsheet2data( $db, $data, \%params, $cgi, $db_table );
                $options{duplicates} = 'update';
            } elsif( exists $params{'jsonfile'} ||
                    (content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE) ) {

                my $json;
                if(  content_type() && content_type() eq $RestfulDB::JSON::MEDIA_TYPE ) {
                    $json = decode( 'UTF-8', $cgi->param( $ENV{REQUEST_METHOD} . 'DATA' ) );
                } else {
                    my $file = $cgi->upload( 'jsonfile' );
                    InputException->throw( "No upload file handle?" ) unless $file;
                    $json = decode( 'UTF-8', join '', <$file> );
                }

                $record_data = json2data( $json, { db => $db,
                                                   default_action => $default_action } );
                for (0..$#$record_data) {
                    my $record_description =
                        $db->get_record_description( $record_data->[$_]{metadata}{table_name},
                                                     { template => 1 } );
                    $record_data->[$_] = merge( $record_description, $record_data->[$_] );
                }
                if( uc($ENV{REQUEST_METHOD}) eq 'POST' ) {
                    $options{duplicates} = 'update'; # UPSERT
                } else {
                    $options{duplicates} = 'ignore';
                }
            } elsif( content_type() && content_type() eq $RestfulDB::JSONAPI::MEDIA_TYPE ) {
                if( uc($ENV{REQUEST_METHOD}) eq 'PUT' ) {
                    InputException->throw( 'HTTP PUT request method is not supported by ' .
                                           'JSON API' );
                }
                my $json = decode( 'UTF-8', $cgi->param('POSTDATA') );
                $record_data = jsonapi2data( $db,
                                             $json,
                                             { default_action => $default_action } );
                my( $offending_entry ) =
                    grep { $_->{metadata}{table_name} ne $db_table }
                         @$record_data;
                if( $offending_entry ) {
                    ConflictException->throw(
                        sprintf( "Cannot %s '%s' by addressing '%s' endpoint",
                                 $default_action,
                                 $offending_entry->{metadata}{table_name},
                                 $db_table ) );
                }
            } elsif( grep { /^column:/ } keys %params ) {
                $record_data =
                    $db->form_parameters_to_descriptions( $data, \%params,
                                                          { default_action => $default_action,
                                                            cgi => $cgi } );
            }

            if( $record_data ) {
                my( $entries, $dbrevision_id ) =
                    $db->modify_record_descriptions( $record_data, \%options );

                my $dbrev_columns = [ $db->get_column_of_kind( $db_table, 'dbrev' ) ];
                if( defined $dbrevision_id && @$dbrev_columns ) {
                    my $comparison = OPTIMADE::Filter::Comparison->new( '=' );
                    $comparison->left( OPTIMADE::Filter::Property->new( lc $dbrev_columns->[0] ) );
                    $comparison->right( $dbrevision_id );
                    $filter = Database::Filter->new_from_tree( $comparison );
                } else {
                    my $uuid_column = $db->get_uuid_column( $db_table );
                    my $root;
                    for my $entry (@$entries) {
                        my( $uid_column ) = grep { exists $entry->{$_} &&
                                                   defined $entry->{$_} }
                                                 ($uuid_column ? $uuid_column : (),
                                                  sort keys %$entry);
                        next if !$uid_column;

                        my $clause = OPTIMADE::Filter::Comparison->new( '=' );
                        $clause->left( OPTIMADE::Filter::Property->new( lc $uid_column ) );
                        $clause->right( $entry->{$uid_column} );
                        if( $root ) {
                            $root = OPTIMADE::Filter::AndOr->new( $root,
                                                                  'OR',
                                                                  $clause );
                        } else {
                            $root = $clause;
                        }
                    }

                    if( $root ) {
                        $filter = Database::Filter->new_from_tree( $root );
                    } else {
                        undef $filter;
                    }
                }

                $params{offset} = 0; # resetting to the first page
            }
        } elsif( uc($ENV{REQUEST_METHOD}) eq 'DELETE' ) {
            my $oath = Authen::OATH->new;
            my $token = $oath->totp( $filter->filter ? $filter->query_string : 'all' );
            my $message = '';
            if( $params{token} && $params{token} eq $token ) {
                my $deleted = $db->delete( $db_table, $filter );
                if( $filter->filter ) {
                    $message = "Records matching filter '" . $filter->query_string .
                               "' ($deleted) were deleted.";
                } else {
                    $message = "All records ($deleted) were deleted.";
                }
            } else {
                if( $params{token} ) {
                    $message = 'Supplied deletion token is either too old, ' .
                               'or incorrect. ';
                }
                $message .=
                    sprintf 'To confirm the deletion of %s, please repeat ' .
                            'the request with "token=%s"',
                            ($filter->filter
                                ? "records matching filter '" .
                                  $filter->query_string . "'"
                                : 'all records'),
                            $token;
            }

            if( $format =~ /^json(api)?$/ ) {
                my %MEDIA_TYPE = (
                    json    => $RestfulDB::JSON::MEDIA_TYPE,
                    jsonapi => $RestfulDB::JSONAPI::MEDIA_TYPE,
                );
                my $meta = $format eq 'json' ? 'metadata' : 'meta';
                print $cgi->header( -type => $MEDIA_TYPE{$format},
                                    -charset => 'UTF-8' ),
                      JSON->new()->canonical->encode( { $meta => { detail => $message } } ),
                      "\n";
                exit;
            } else {
                # TODO: this should probably be reported as a notice, not warning.
                warn "$message\n";
            }
        } elsif( uc($ENV{REQUEST_METHOD}) ne 'GET' ) {
            InputException->throw(
                "Unknown HTTP request method '$ENV{REQUEST_METHOD}'. " .
                "Please contact your Web site administrator to fix this problem." );
        }

        my $order = Database::Order->new_from_string( $params->{order},
                                                      $db_table );

        my $get_record_descriptions_options = {
            id_column    => $params{id_column},
            filter       => $filter,
            order        => $order,
            web_base     => dirname( $ENV{REQUEST_URI} ),
            rows         => $params{rows},
            offset       => $params{offset},
            depth        => $params{depth},
            no_empty     => 1,
        };

        if( defined $format && $format =~ /^(csv|csv\s*zip|ods|xlsx?)$/ ) {
            my $requested_tables;
            if( exists $params->{include} ) {
                @$requested_tables = split ',', $params->{include};
            }
            my $record_descriptions =
                $db->get_record_descriptions( $db_table,
                                              {
                                                  %$get_record_descriptions_options,
                                                  requested_tables => $requested_tables,
                                                  no_empty => 0,
                                              } );
            if( !@$record_descriptions ) {
                $record_descriptions =
                    $db->get_record_descriptions( $db_table,
                                                  {
                                                    %$get_record_descriptions_options,
                                                    requested_tables => $requested_tables,
                                                    template => 1,
                                                    no_empty => 0,
                                                    no_foreign => 1,
                                                  } );
            }

            if( $format eq 'xlsx' ) {
                open my $out, '>', \my $output;
                my $spreadsheet = Excel::Writer::XLSX->new( $out );
                eval {
                    data2spreadsheet( $spreadsheet, $record_descriptions );
                };
                NotImplementedException->throw( $@ ) if $@ && $@ =~ /must be <= 31 chars/;
                $spreadsheet->close;

                binmode( STDOUT );
                print $cgi->header( -type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                                    -expires => 'now',
                                    -attachment => "$db_table.xlsx" ),
                      $output;
            } elsif( $format eq 'ods' || $format eq 'xls' ) {
                my $file = File::Temp->new;
                my( $spreadsheet, $type );
                if( $format eq 'ods' ) {
                    $spreadsheet = Spreadsheet::Wright::OpenDocument->new( file => $file,
                                                                           format => $format,
                                                                           sheet => $db_table );
                    $type = 'application/vnd.oasis.opendocument.spreadsheet';
                } else {
                    $spreadsheet = Spreadsheet::Wright::Excel->new( file => $file,
                                                                    format => $format );
                    $type = 'application/vnd.ms-excel';
                }
                eval {
                    data2spreadsheet( $spreadsheet, $record_descriptions );
                };
                NotImplementedException->throw( $@ ) if $@ && $@ =~ /must be <= 31 chars/;

                $spreadsheet->close;
                binmode( STDOUT );
                print $cgi->header( -type => $type,
                                    -expires => 'now',
                                    -attachment => "$db_table.$format" );
                open( my $inp, $file );
                while( <$inp> ) {
                    print;
                }
                close $inp;
            } elsif( $format eq 'csv' ) {
                my $spreadsheet = '';
                data2spreadsheet( \$spreadsheet, $record_descriptions );
                print $cgi->header( -type => 'text/csv',
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table.csv" ),
                      $spreadsheet;
            } elsif( $format eq 'csvzip' ) {
                my $zip = Archive::Zip->new();
                data2spreadsheet( $zip, $record_descriptions );
                print $cgi->header( -type => 'application/zip',
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table.zip" );
                $zip->writeToFileHandle( *STDOUT, 0 );
            }
        } elsif( defined $format && $format =~ /^json(api)?$/ ) {
            $params{depth} =  0 if $format eq 'json' && !exists $params{depth};
            $params{depth} = -1 if $format eq 'jsonapi';
            my $record_descriptions =
                $db->get_record_descriptions( $db_table,
                                              {
                                                  %$get_record_descriptions_options,
                                                  depth => $params{depth},
                                              } );
            if( $format eq 'json' ) {
                print $cgi->header( -type => $RestfulDB::JSON::MEDIA_TYPE,
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table.json" ),
                      data2json( $db, $record_descriptions, $db_table,
                                 { request_uri => $ENV{REQUEST_URI},
                                   data_matched =>
                                    $db->get_count( $db_table, $filter ),
                                   web_base => dirname( url() ),
                                   warnings => \@warnings } ),
                      "\n";
            } else {
                print $cgi->header( -type => $RestfulDB::JSONAPI::MEDIA_TYPE,
                                    -expires => 'now',
                                    -charset => 'UTF-8',
                                    -attachment => "$db_table.json" ),
                      data2collection( $db, $record_descriptions, $db_table,
                                       { request_uri => $ENV{REQUEST_URI},
                                         web_base => dirname( url() ),
                                         warnings => \@warnings } ),
                      "\n";
            }
        } elsif( !defined $format || $format eq 'html' ) {
            my $html;

            $html = $cgi->header(-type=>"text/html", -expires=>'now', -charset=>'UTF-8');

            $html .= start_html5( $cgi,
                        {
                            -title => "$db_table table - $db_name db: " .
                                      $RestfulDB::Defaults::website_title,
                            -head => Link({
                                           -rel  => 'icon',
                                           -type => 'image/x-icon',
                                           -href => '../images/favicon.ico'
                            }),
                            -style => get_css_styles( $db_name, $page_level ),
                            -meta => {
                              'viewport' => 'width=device-width, initial-scale=1',
                              'description' => 'Short restful db description',
                              'keywords' => join( ', ', @RestfulDB::Defaults::keywords ),
                              'author' => 'Restful Authors'},
                            -script => [
                                        {-src  => "../js/jquery-3.3.1.min.js" },
                                        {-src  => '../js/select2.js' },
                                        {-src  => "../js/set_table_maxheight.js" },
                                        {-src  => "../js/form_control.js" }
                            ]
                        }
                    );

            # The beginning of the HTML page.
            # Open Main containers.
            $html .= start_root();
            $html .= header_db_pl( $db_name, $db_table );
            $html .= start_doc_wrapper();
            $html .= start_main_content();

            # Hidden 'top' paragraph on the data page for the 'To top' link.
            $html .= p( {-id => "top"}, "Top" ) . "\n";

            # Warnings
            $html .= warnings2html( @warnings );

            # Acknowledge the user name:
            if( defined $remote_user ) {
                $html .= start_card_fluid( { id => "username-card" } );
                # 'doc' class for paragraphs ensures that their content
                # can be center-aligned.
                $html .= p( {-class => "doc"}, "User: $remote_user" ) . "\n";
                $html .= end_card();
            }

            $html .= start_card_fluid(); # main data card.

            $html .= start_div( {-class => "section",
                                 -id => "search-form-section"} ) . "\n";

            $html .= two_column_row_html({
                            column_major => "Table: $db_table",
                            row_class => "page-title"
                        });

            # Output the search form:
            my $search_form_uri = transfer_query_string(
                $base_uri, $ENV{REQUEST_URI},
                { exclude_re => 'select_.*|search_value|.cgifields',
                  append => $filter ? $filter->query_string_uri : '' }
                );

            my $default_select_column;
            my $default_not_operator = '';
            my $default_select_operator = 'eq';
            my $default_search_value;
            my $default_select_combining = 'new';

            if( $filter && ref $filter ) {
                my $rightmost = $filter->rightmost;
                if( $rightmost ) {
                    my %columns = map { ( lc $_ => $_ ) } @columns;
                    if( $rightmost->isa( OPTIMADE::Filter::Negation:: ) ) {
                        $default_not_operator = 'not';
                        $rightmost = $rightmost->inner;
                    }
                    if( $rightmost->isa( OPTIMADE::Filter::Known:: ) &&
                        ( @{$rightmost->property} == 1 ||
                         (@{$rightmost->property} == 2 &&
                          $rightmost->property->[0] eq $db_table)) ) {
                        $default_select_column = $columns{$rightmost->property->[-1]};
                        $default_select_operator =
                            $rightmost->is_known ? 'known' : 'unknown';
                    }
                    if( $rightmost->isa( OPTIMADE::Filter::Comparison:: ) ) {
                        my %reverse_operators = reverse %{$Database::filter_operators};

                        if( ref $rightmost->left &&
                            $rightmost->left->isa( OPTIMADE::Filter::Property:: ) ) {
                            if( @{$rightmost->left} == 1 ||
                               (@{$rightmost->left} == 2 &&
                                $rightmost->left->[0] eq $db_table ) ) {
                                $default_select_column = $columns{$rightmost->left->[-1]};
                                $default_select_operator = $reverse_operators{lc $rightmost->{operator}};
                                $default_search_value = $rightmost->right;
                            }
                        }
                    }
                }
                my $topmost_operation = $filter->topmost_operation;
                if( $topmost_operation ) {
                    if(      $topmost_operation eq 'AND' ) {
                        $default_select_combining = 'within';
                    } elsif( $topmost_operation eq 'OR' ) {
                        $default_select_combining = 'append';
                    }
                }
            }

            # Put spreadsheet forms inside 'row' containers:
            # 'row1' : search form and 'row2' toggle buttons;
            # 'row2' : download and upload panels.

            # Search form outer block.
            $html .= start_div( { -class => 'row',
                                  -id => '#spreadsheet-row1' } ) .
                     start_div( { -class => 'col-sm-12 col-md-12 col-lg-12' } );

            $html .= start_form_QS(
                -action => $search_form_uri,
                -method => 'get',
                -class => 'form_init' ) . "\n";

            my $fk_columns  = $db->get_foreign_keys($db_table);
            my $fk_list = [ map{ $_->name() ne 'revision_id' ?
                                    $_->name() : () } @{$fk_columns} ];
            
            my $fk_json = encode_json($fk_list);

            # my $fks_cell_text = encode_json($db->get_fks_cell_text($fk_columns) );

            do {
                local $" = "\n";
                $html .= popup_menu( -name => 'select_column',
                                     -values => \@columns,
                                     -default => $default_select_column,
                                     -onchange => "search_suggest($fk_json)" ) . "\n" .

                         popup_menu( -name => 'select_not_operator',
                                     -values => [ '', 'not' ],
                                     -labels => { '' => '', 'not' => 'NOT' },
                                     -default => $default_not_operator ) . "\n" .

                         popup_menu( -name => 'select_operator',
                                     -id => 'select_operator',
                                     -values => [ sort {$Database::filter_operators->{$a}
                                                        cmp
                                                        $Database::filter_operators->{$b}}
                                                  keys %$Database::filter_operators ],
                                     -labels => $Database::filter_operators,
                                     -default => [ $default_select_operator ],
                                     -onchange => 'toggle_search_value()' ) . "\n";
            };
            
            $html .= textfield( { -id =>'search_text',
                                  -name => 'search_value',
                                  -value => $default_search_value }) . "\n";
            #my $fks_cell_text = $db->get_fks_cell_text( $fk_columns );
            my $fk_table_data = $db->get_fks_cell_text( $fk_columns );
            my $ambiguous_fks = {};
            foreach my $fk( sort keys %{ $fk_table_data->{fk_values} } ){
                my $fk_table_values = $fk_table_data->{fk_values}{$fk};  
                $ambiguous_fks->{ $fk } = 
                    HTMLGenerator::get_ambiguous_fk_values($fk_table_values, $fk, $fk_columns);
            }

             $html .= HTMLGenerator::convert_fk_to_select( $fk_table_data->{fk_values},
                                                              $ambiguous_fks,
                                                              $fk_columns);

            # Set attributes for the radios inside the group.
            my %attributes = (
                new => {'title' => 'Initiate new filter'},
                append => {'title' => 'Logical \'or\''},
                within => {'title' => 'Logical \'and\''}
            );
            $html .= start_span( { -class => 'radio-group' } ) .
                     radio_group( -name => 'select_combining',
                                  -attributes => \%attributes,
                                  -values => [ 'new', 'append', 'within' ],
                                  -default => $default_select_combining ) .
                     "\n" .
                     end_span();

            # Sets responsive margins and padding for a button.
            $html .= nameless_submit( 'Filter') .
            # $html .= input( {-type => 'submit',
            #                 -value => 'Filter',
            #                 -class => button_classes_string(),
            #                 -disable => ""
            #                 } ).
                    "\n" .
                     end_form .
                     "\n"; # End search form.

            $html .= end_div() . end_div(); # Close row and column containers.

            # Setting collapsible anchors for the download and upload forms.
            # Set spreadsheet panels checkbox to 'not checked'.
            $html .= input( { -type => "checkbox",
                              -id => "expandable:spreadsheet-panels",
                              ##-checked => "",
                              -class => "isexpanded" } ) .
                     label( { -for => "expandable:spreadsheet-panels",
                              -class => "expandable" },
                              start_span() .
                              end_span() .
                              " Data download and upload panel" );

            my $filter_qs = $filter->query_string_uri if $filter;

            # Spreadsheet download form.
            my $template_form_uri =
                transfer_query_string(
                    $base_uri,
                    $ENV{REQUEST_URI},
                    { exclude_re => 'select_.*|search_value|.cgifields|rows|offset',
                      append => $filter_qs },
                );

            $html .= start_div( { -class => 'related expandable row',
                                  -for => "spreadsheet-panels",
                                  -id => 'spreadsheet-panels' } ) .
                     start_div( { -class => 'col-sm-12 col-md-12 col-lg-12' } );
            $html .= start_form_QS( -action => $template_form_uri,
                                    -method => 'get',
                                    -class => 'form_init' ) .
                     popup_menu( -name => 'format',
                                 -values => [ 'csv', 'csv zip', 'ods', 'xls', 'xlsx' ],
                                 -default => 'csv',
                                 -labels => { 'csv' => 'CSV',
                                              'csv zip' => 'CSV+ZIP',
                                              'ods' => 'ODS',
                                              'xls' => 'XLS',
                                              'xlsx' => 'XLSX' } ) .
                     nameless_submit( 'Download data' ) .
                     hidden( { -name => 'depth', -value => 'auto' } ) .
                     'Rows in data file:' .
                     textfield( -name => 'rows', -default => $params{rows}, -size => 5 ) .
                     '<button name="action" class="' . button_classes_string() .
                     '" type="submit" value="template">Download template' .
                     '</button>' .
                     'Rows in template file:' .
                     textfield( -name => 'rows_template', -default => 0, -size => 5 ) .
                     end_form;

            # Spreadsheet upload form.
            if( !$db->is_view( $db_table ) ) {
                $html .= start_form( -method => 'post',
                                     -class => 'form_init' ) .
                         popup_menu( -name => 'upload_format',
                                     -values => [ 'autodetect', 'csv', 'csv zip',
                                                  'ods', 'xls', 'xlsx' ],
                                     -default => 'autodetect',
                                     -labels => { 'autodetect' => 'Auto-detect',
                                                  'csv' => 'CSV',
                                                  'csv zip' => 'CSV+ZIP',
                                                  'ods' => 'ODS',
                                                  'xls' => 'XLS',
                                                  'xlsx' => 'XLSX' } ) .
                         '<button name="action" class="' . button_classes_string() .
                         '" type="submit" value="upload">Upload data' .
                         '</button>' .
                         filefield( -name => 'spreadsheet' ) .
                         end_form;
            }

            # Close row and column containers and then the first section.
            $html .= end_div() . end_div() . end_div() . "\n";

            # Start another section.
            $html .= start_div( {-class => "section",
                                 -id => "search-results-section"} )
                                 . "\n";

            # Pagination text.
            my $nrecords = $db->get_count( $db_table, $filter );
            my $page_offset = int($params{offset} / $params{rows}); # Zero based
            my $current_page = $page_offset + 1; # One based
            my $total_pages = int($nrecords / $params{rows}) +
                ($nrecords % $params{rows} == 0 ? 0 : 1);

            # There cannot be 1 page of 0 total pages so, this special case is solved.
            if( $total_pages == 0 ) { $current_page = 0; }

            my $paging_text =
                "Page $current_page of $total_pages" . "\n";

            my $records_text =
                "$nrecords records. "
                . "Up to $params{rows} records per page"
                . "\n";

            my $back_to_no_filter_button;
            # Scale the '$paging_text' column without the filter selection text.
            my ( $scale_lg, $scale_md ) = ( 5, 6 );

            if( $filter && $filter->filter ) {
                my $where_description = $filter->query_string();
                $paging_text .= ' selected with ' .
                    span( { -class => 'filter' },
                          encode_entities( $where_description ) );

                # Scale the '$paging_text' column if filter selection text present.
                ( $scale_lg, $scale_md ) = ( 10, 8 );

                my $all_records_uri =
                    transfer_query_string( $base_uri,
                                           $ENV{REQUEST_URI},
                                           { exclude_re =>
                                                 'select_.*|search_value|' .
                                                 'filter|.cgifields' } );
                # Former link 'Show all records' now displayed as a
                # '$back_to_no_filter_button' button.
                $back_to_no_filter_button =
                    a( { -href => $all_records_uri,
                         -title => "Remove current filter",
                         -class => button_classes_string('primary') },
                         'Clear filter' );
            } else {
                # Add the disabled 'Clear filter' button on the page.
                $back_to_no_filter_button =
                    a( { -href => "#",
                         -title => "Select a filter to activate",
                         -class => button_classes_string('primary disabled') },
                         'Clear filter' );
            }
            $paging_text .= $back_to_no_filter_button;
            # end Pagination text.

            # Set the variables to split html elements.
            my ( $prev_button_form, $next_button_form );

            my $prev_offset = 0;
            my $next_offset = 0;
            my $prev_button_submit;
            my $next_button_submit;
            if( $total_pages > 1 ) {
                my $last_page = $total_pages - 1;

                $prev_offset =
                    ($page_offset > 0 ? ($page_offset - 1) : $last_page) *
                    $params{rows};
                $prev_offset = 0 if $prev_offset < 0;

                $next_offset =
                    ($page_offset < $last_page ? $page_offset + 1 : 0) *
                    $params{rows};

                $prev_button_submit =
                    nameless_submit( 'Prev', "Previous page" );
                $next_button_submit =
                    nameless_submit( 'Next', "Next page" );

            } else {
                $prev_button_submit =
                    nameless_submit_disabled( 'Prev', "Inactive previous page" );
                $next_button_submit =
                    nameless_submit_disabled( 'Next', "Inactive next page" );
            }

            # FIXME:
            # Need to rethink 'forward_backward_control_form' argument
            # processing, as 'transfer_query_string' for the disabled button
            # is not necessary.
            $prev_button_form = forward_backward_control_form(
                transfer_query_string( $base_uri, $ENV{REQUEST_URI},
                                       { append => "offset=$prev_offset" .
                                             ( defined $params{rows} ?
                                               "&rows=$params{rows}" : '') .
                                             ( $filter_qs ? '&' . $filter_qs : '' ),
                                             exclude_re =>
                                             'select_.*|search_value|Prev|' .
                                             'Next|.cgifields' } ),
                $prev_button_submit );

            # FIXME:
            # Need to rethink 'forward_backward_control_form' argument
            # processing, as 'transfer_query_string' for the disabled button
            # is not necessary.
            my $new_offset = 0;
            $next_button_form = forward_backward_control_form(
                transfer_query_string( $base_uri, $ENV{REQUEST_URI},
                                       { append => "offset=$next_offset" .
                                             ( defined $params{rows} ?
                                               "&rows=$params{rows}" : '') .
                                             ( $filter_qs ? '&' . $filter_qs : '' ),
                                             exclude_re =>
                                             'select_.*|search_value|Prev|' .
                                             'Next|.cgifields' } ),
                $next_button_submit );

            my $data =
                $db->get_record_descriptions( $db_table,
                                              {
                                                  %$get_record_descriptions_options,
                                                  no_related => 1,
                                              } );

            my $request_uri =
                transfer_query_string( $base_uri,
                                       $base_uri,
                                       { append => { map { $_ => $params{$_} }
                                                     grep { $changed->{$_} &&
                                                            !/^((csv|ods|xlsx?|json)file|spreadsheet)$/ &&
                                                            !/^column:/ }
                                                     keys %params } } );
            my ( $table_html, $new_record_button ) = data2html_list_table(
                $db,
                $data,
                $db_table,
                {
                    columns => \@columns,
                    id_column => $params{id_column},
                    ignore_empty_fields => 1,
                    make_sort_buttons => 1,
                    order => $order,
                    request_uri => $request_uri,
                    table_properties =>
                        { -class => 'hoverable', -id => 'mydb-table' },
                    ambiguous_fks => $ambiguous_fks 
                }
            );

            # Assemble the HTML.
            # Start controls row.
            # 'scale_sm => 6' sets the buttons columns to comfortable width,
            # so as not too narrow on especially small screens.
            my $recordnav_left_column_html =
                three_column_row_html({ column_left => $prev_button_form,
                                        column_middle => $paging_text,
                                        column_right => $next_button_form,
                                        scale_lg => $scale_lg,
                                        scale_md => $scale_md,
                                        scale_sm => 6
                                     });

            ### 'scale' parameter meaning : signifies the column dimension class for the
            ###                             MINOR column.
            ###                             So the major column fills up all of the remaining
            ###                             row space.
            ##my $recordnav_html =
            ##    two_column_row_html({ column_major => $recordnav_left_column_html,
            ##                          column_minor => $back_to_no_filter_button,
            ##                          scale_lg => 2,
            ##                          scale_md => 2,
            ##                          scale_sm => 12
            ##                      });

            # $new_record_html as the right column to be replaced by the breadcrumbs
            # that for now are in the footer.
            my $new_record_html = two_column_row_html({
                            column_major => p( {-class => "doc"}, $records_text),
                            column_minor => $new_record_button,
                            scale_lg => 1,
                            scale_md => 1,
                            scale_sm => 1,
                            invert_columns => 1,
                            new_absolute => 1
                        });

            $html .= two_column_row_html({
                column_major => $recordnav_left_column_html,
                column_minor => $new_record_html,
                scale_lg => 6,
                row_id => "table-nav-panel",
                invert_columns => 1
            });
            # end controls row

            # Add the main database table class and id.
            #
            # Outer row container now is needed beacuse the default tables
            # can't grow up to the maximum page space, when the table is too
            # small in width. As a consecuence 'table-conatrainer' scrollable
            # bar becomes detached from the data table outer right border.
            # 'row' container is essentially a flex container that can shrink
            # according to its children content.
            $html .= start_div( {-class => "row"} ) .
                     $table_html .
                     end_div();

            # This ends an HTML document by printing the </body></html> tags.
            $html .= end_div() . "\n"; # end section
            $html .= end_card();
            $html .= footer_db_pl( $db_name, $db_table );
            $html .= end_main_content();
            $html .= end_doc_wrapper();
            $html .= end_root() . "\n";
            $html .=  end_html . "\n";

            print $html;
        } else {
            InputException->throw( "Unsupported format '$format'" );
        }
    }

    $db->disconnect();
};

if( $@ ) {
    if(      $format && $format eq 'json' ) {
        error2json( $cgi, $@ );
    } elsif( $format && $format eq 'jsonapi' ) {
        error2jsonapi( $cgi, $@ );
    } else {
        error_page( $cgi, $@, $page_level );
    }
}
