BaseAdaptor.pm 21.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=pod 

=head1 NAME

    Bio::EnsEMBL::Hive::DBSQL::BaseAdaptor

=head1 DESCRIPTION

    The base class for all other Object- or NakedTable- adaptors.
    Performs the low-level SQL needed to retrieve and store data in tables.

=head1 EXTERNAL DEPENDENCIES

    DBI 1.6

=head1 LICENSE

18
    Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
19
20
21
22
23
24
25
26
27
28
29
30

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the License
    is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and limitations under the License.

=head1 CONTACT

31
    Please subscribe to the Hive mailing list:  http://listserver.ebi.ac.uk/mailman/listinfo/ehive-users  to discuss Hive-related questions or to be notified of our updates
32
33
34
35

=cut


36
37
38
package Bio::EnsEMBL::Hive::DBSQL::BaseAdaptor;

use strict;
39
use warnings;
40
no strict 'refs';   # needed to allow AUTOLOAD create new methods
41
use DBI 1.6;        # the 1.6 functionality is important for detecting autoincrement fields and other magic.
42

43
use Bio::EnsEMBL::Hive::Utils ('stringify', 'throw');
44

45
46
47
48
49
50
51
52
53
54
55

sub default_table_name {
    die "Please define table_name either by setting it via table_name() method or by redefining default_table_name() in your adaptor class";
}


sub default_insertion_method {
    return 'INSERT_IGNORE';
}


56
57
58
59
sub default_overflow_limit {
    return {
        # 'overflow_column1_name' => column1_size,
        # 'overflow_column2_name' => column2_size,
60
61
62
63
64
65
66
67
68
        # ...
    };
}

sub default_input_column_mapping {
    return {
        # 'original_column1' => "original_column1*10 AS c1_times_ten",
        # 'original_column2' => "original_column2+1 AS c2_plus_one",
        # ...
69
70
71
    };
}

72
73
74
# ---------------------------------------------------------------------------

sub new {
75
76
    my $class   = shift @_;
    my $dbobj   = shift @_;
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

    my $self = bless {}, $class;

    if ( !defined $dbobj || !ref $dbobj ) {
        throw("Don't have a db [$dbobj] for new adaptor");
    }

    if ( ref($dbobj) =~ /DBConnection$/ ) {
        $self->dbc($dbobj);
    } elsif( UNIVERSAL::can($dbobj, 'dbc') ) {
        $self->dbc( $dbobj->dbc );
        $self->db( $dbobj );
    } else {
        throw("I was given [$dbobj] for a new adaptor");
    }

93
94
95
96
97
98
    my %flags = @_;

    if(my $table_name = delete $flags{ 'table_name' }) {
        $self->table_name( $table_name );
    }

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
    return $self;
}


sub db {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_db} = shift @_;
    }
    return $self->{_db};
}


sub dbc {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_dbc} = shift @_;
    }
    return $self->{_dbc};
}


sub prepare {
    my ( $self, $sql ) = @_;

    # Uncomment next line to cancel caching on the SQL side.
    # Needed for timing comparisons etc.
    #$sql =~ s/SELECT/SELECT SQL_NO_CACHE/i;

    return $self->dbc->prepare($sql);
}

133
134
135
136
137
138
139
140
141
142
143

sub overflow_limit {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_overflow_limit} = shift @_;
    }
    return $self->{_overflow_limit} || $self->default_overflow_limit();
}


144
145
146
147
148
149
150
151
152
153
sub input_column_mapping {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_input_column_mapping} = shift @_;
    }
    return $self->{_input_column_mapping} || $self->default_input_column_mapping();
}


154
155
156
157
158
sub table_name {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_table_name} = shift @_;
159
        $self->_table_info_loader();
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
    }
    return $self->{_table_name} || $self->default_table_name();
}


sub insertion_method {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_insertion_method} = shift @_;
    }
    return $self->{_insertion_method} || $self->default_insertion_method();
}


sub column_set {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_column_set} = shift @_;
    } elsif( !defined( $self->{_column_set} ) ) {
        $self->_table_info_loader();
    }
    return $self->{_column_set};
}


sub primary_key {        # not necessarily auto-incrementing
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_primary_key} = shift @_;
    } elsif( !defined( $self->{_primary_key} ) ) {
        $self->_table_info_loader();
    }
    return $self->{_primary_key};
}


sub updatable_column_list {    # it's just a cashed view, you cannot set it directly
    my $self = shift @_;

    unless($self->{_updatable_column_list}) {
        my %primary_key_set = map { $_ => 1 } @{$self->primary_key()};
        my $column_set      = $self->column_set();
        $self->{_updatable_column_list} = [ grep { not $primary_key_set{$_} } keys %$column_set ];
    }
    return $self->{_updatable_column_list};
}


sub autoinc_id {
    my $self = shift @_;

    if(@_) {    # setter
        $self->{_autoinc_id} = shift @_;
    } elsif( !defined( $self->{_autoinc_id} ) ) {
        $self->_table_info_loader();
    }
    return $self->{_autoinc_id};
}


sub _table_info_loader {
    my $self = shift @_;

    my $dbc         = $self->dbc();
227
    my $dbh         = $dbc->db_handle();
228
    my $driver      = $dbc->driver();
229
    my $dbname      = $dbc->dbname();
230
231
232
233
    my $table_name  = $self->table_name();

    my %column_set  = ();
    my $autoinc_id  = '';
234
235
236
237
238
    my @primary_key = $dbh->primary_key(undef, undef, $table_name);

    my $sth = $dbh->column_info(undef, undef, $table_name, '%');
    $sth->execute();
    while (my $row = $sth->fetchrow_hashref()) {
239
240
241
        my ( $column_name, $column_type ) = @$row{'COLUMN_NAME', 'TYPE_NAME'};

        # warn "ColumnInfo [$table_name/$column_name] = $column_type\n";
242

243
        $column_set{$column_name}  = $column_type;
244

245
        if( ($column_name eq $table_name.'_id')
246
         or ($table_name eq 'analysis_base' and $column_name eq 'analysis_id') ) {    # a special case (historical)
247
            $autoinc_id = $column_name;
248
249
250
251
252
253
254
255
256
257
258
        }
    }
    $sth->finish;

    $self->column_set(  \%column_set );
    $self->primary_key( \@primary_key );
    $self->autoinc_id(   $autoinc_id );
}


sub count_all {
259
    my ($self, $constraint, $key_list) = @_;
260
261
262

    my $table_name      = $self->table_name();

263
    my $sql = "SELECT ".($key_list ? join(', ', @$key_list, '') : '')."COUNT(*) FROM $table_name";
264

265
266
267
    if($constraint) {
            # in case $constraint contains any kind of JOIN (regular, LEFT, RIGHT, etc) do not put WHERE in front:
        $sql .= (($constraint=~/\bJOIN\b/i) ? ' ' : ' WHERE ') . $constraint;
268
269
    }

270
271
272
    if($key_list) {
        $sql .= " GROUP BY ".join(', ', @$key_list);
    }
273
    # warn "SQL: $sql\n";
274
275

    my $sth = $self->prepare($sql);
276
277
278
279
280
281
282
283
284
285
286
287
288
289
    $sth->execute;

    my $result_struct;  # will be autovivified to the correct data structure

    while(my $hashref = $sth->fetchrow_hashref) {

        my $pptr = \$result_struct;
        if($key_list) {
            foreach my $syll (@$key_list) {
                $pptr = \$$pptr->{$hashref->{$syll}};   # using pointer-to-pointer to enforce same-level vivification
            }
        }
        $$pptr = $hashref->{'COUNT(*)'};
    }
290

291
292
293
294
295
296
297
298
299
    unless(defined($result_struct)) {
        if($key_list and scalar(@$key_list)) {
            $result_struct = {};
        } else {
            $result_struct = 0;
        }
    }

    return $result_struct;
300
301
302
303
}


sub fetch_all {
304
305
    my ($self, $constraint, $one_per_key, $key_list, $value_column) = @_;
    
306
307
    my $table_name              = $self->table_name();
    my $input_column_mapping    = $self->input_column_mapping();
308

309
    my $sql = 'SELECT ' . join(', ', map { $input_column_mapping->{$_} // "$table_name.$_" } keys %{$self->column_set()}) . " FROM $table_name";
310
311

    if($constraint) { 
312
            # in case $constraint contains any kind of JOIN (regular, LEFT, RIGHT, etc) do not put WHERE in front:
313
        $sql .= (($constraint=~/\bJOIN\b/i or $constraint=~/^LIMIT|ORDER|GROUP/) ? ' ' : ' WHERE ') . $constraint;
314
315
    }

316
    # warn "SQL: $sql\n";
317
318
319
320

    my $sth = $self->prepare($sql);
    $sth->execute;  

321
322
323
    my @overflow_columns = keys %{ $self->overflow_limit() };
    my $overflow_adaptor = scalar(@overflow_columns) && $self->db->get_AnalysisDataAdaptor();

324
    my $result_struct;  # will be autovivified to the correct data structure
325
326

    while(my $hashref = $sth->fetchrow_hashref) {
327
328
329
330
331
332
333

        foreach my $overflow_key (@overflow_columns) {
            if($hashref->{$overflow_key} =~ /^_ext(?:\w+)_data_id (\d+)$/) {
                $hashref->{$overflow_key} = $overflow_adaptor->fetch_by_analysis_data_id_TO_data($1);
            }
        }

334
        my $pptr = \$result_struct;
335
336
337
338
        if($key_list) {
            foreach my $syll (@$key_list) {
                $pptr = \$$pptr->{$hashref->{$syll}};   # using pointer-to-pointer to enforce same-level vivification
            }
339
340
341
342
        }
        my $object = $value_column
            ? $hashref->{$value_column}
            : $self->objectify($hashref);
343
344
345
346
347

        if(UNIVERSAL::can($object, 'seconds_since_last_fetch')) {
            $object->seconds_since_last_fetch(0);
        }

348
349
        if($one_per_key) {
            $$pptr = $object;
350
        } else {
351
            push @$$pptr, $object;
352
353
354
355
        }
    }
    $sth->finish;  

356
    unless(defined($result_struct)) {
357
        if($key_list and scalar(@$key_list)) {
358
359
360
361
362
363
364
            $result_struct = {};
        } elsif(!$one_per_key) {
            $result_struct = [];
        }
    }

    return $result_struct;  # either listref or hashref is returned, depending on the call parameters
365
366
367
368
}


sub primary_key_constraint {
369
370
    my $self        = shift @_;
    my $sliceref    = shift @_;
371
372
373
374

    my $primary_key  = $self->primary_key();  # Attention: the order of primary_key columns of your call should match the order in the table definition!

    if(@$primary_key) {
375
        return join (' AND ', map { $primary_key->[$_]."='".$sliceref->[$_]."'" } (0..scalar(@$primary_key)-1));
376
377
378
379
380
381
382
383
384
385
    } else {
        my $table_name = $self->table_name();
        die "Table '$table_name' doesn't have a primary_key";
    }
}


sub fetch_by_dbID {
    my $self = shift @_;    # the rest in @_ should be primary_key column values

386
    return $self->fetch_all( $self->primary_key_constraint( \@_ ), 1 );
387
388
389
}


390
391
392
sub remove_all {    # remove entries by a constraint
    my $self        = shift @_;
    my $constraint  = shift @_ || 1;
393

394
    my $table_name  = $self->table_name();
395

396
    my $sql = "DELETE FROM $table_name WHERE $constraint";
397
398
399
400
401
402
    my $sth = $self->prepare($sql);
    $sth->execute();
    $sth->finish();
}


403
404
405
406
407
408
409
410
411
412
sub remove {    # remove the object by primary_key
    my $self        = shift @_;
    my $object      = shift @_;

    my $primary_key_constraint  = $self->primary_key_constraint( $self->slicer($object, $self->primary_key()) );

    return $self->remove_all( $primary_key_constraint );
}


413
414
415
416
417
418
419
420
421
422
423
424
425
sub update {    # update (some or all) non_primary columns from the primary
    my $self    = shift @_;
    my $object  = shift @_;    # the rest in @_ should be the column names to be updated

    my $table_name              = $self->table_name();
    my $primary_key_constraint  = $self->primary_key_constraint( $self->slicer($object, $self->primary_key()) );
    my $columns_to_update       = scalar(@_) ? \@_ : $self->updatable_column_list();
    my $values_to_update        = $self->slicer( $object, $columns_to_update );

    unless(@$columns_to_update) {
        die "There are no dependent columns to update, as everything seems to belong to the primary key";
    }

426
    my $sql = "UPDATE $table_name SET ".join(', ', map { "$_=?" } @$columns_to_update)." WHERE $primary_key_constraint";
427
    # warn "SQL: $sql\n";
428
    my $sth = $self->prepare($sql);
429
    # warn "VALUES_TO_UPDATE: ".join(', ', map { "'$_'" } @$values_to_update)."\n";
430
431
    $sth->execute( @$values_to_update);

432
433
434
    $sth->finish();
}

435

436
sub store_or_update_one {
437
    my ($self, $object, $filter_columns) = @_;
438

439
    #use Data::Dumper;
440
    if(UNIVERSAL::can($object, 'adaptor') and $object->adaptor and $object->adaptor==$self) {  # looks like it has been previously stored
441
        if( @{ $self->primary_key() } and @{ $self->updatable_column_list() } ) {
442
            $self->update( $object );
443
444
445
            #warn "store_or_update_one: updated [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
        } else {
            #warn "store_or_update_one: non-updatable [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
446
        }
447
    } elsif( my $present = $self->check_object_present_in_db_by_content( $object, $filter_columns ) ) {
448
        $self->mark_stored($object, $present);
449
450
451
452
453
        #warn "store_or_update_one: found [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."] in db by content of (".join(', ', @$filter_columns).")\n";
        if( @{ $self->primary_key() } and @{ $self->updatable_column_list() } ) {
            #warn "store_or_update_one: updating the columns (".join(', ', @{ $self->updatable_column_list() }).")\n";
            $self->update( $object );
        }
454
455
    } else {
        $self->store( $object );
456
        #warn "store_or_update_one: stored [".(UNIVERSAL::can($object, 'toString') ? $object->toString : Dumper($object))."]\n";
457
458
459
    }
}

460

461
462
sub check_object_present_in_db_by_content {    # return autoinc_id/undef if the table has autoinc_id or just 1/undef if not
    my ( $self, $object, $filter_columns ) = @_;
463
464
465
466
467

    my $table_name  = $self->table_name();
    my $column_set  = $self->column_set();
    my $autoinc_id  = $self->autoinc_id();

468
469
470
471
472
473
474
475
476
    if($filter_columns) {
            # make sure all fields exist in the database as columns:
        $filter_columns = [ map { $column_set->{$_} ? $_ : $_.'_id' } @$filter_columns ];
    } else {
            # we look for identical contents, so must skip the autoinc_id columns when fetching:
        $filter_columns = [ grep { $_ ne $autoinc_id } keys %$column_set ];
    }
    my %filter_hash;
    @filter_hash{ @$filter_columns } = @{ $self->slicer( $object, $filter_columns ) };
477

478
479
    my @constraints = ();
    my @values = ();
480
    while(my ($column, $value) = each %filter_hash ) {
481
482
483
484
485
486
487
        if( defined($value) ) {
            push @constraints, "$column = ?";
            push @values, $value;
        } else {
            push @constraints, "$column IS NULL";
        }
    }
488

489
490
491
    my $sql = 'SELECT '.($autoinc_id or 1)." FROM $table_name WHERE ".  join(' AND ', @constraints);
    my $sth = $self->prepare( $sql );
    $sth->execute( @values );
492

493
    my ($return_value) = $sth->fetchrow_array();
494
#warn "check_object_present_in_db_by_content: sql= $sql WITH VALUES (".join(', ', @values).") ---> return_value=".($return_value//'undef')."\n";
495
496
497
498
499
500
501
    $sth->finish;

    return $return_value;
}


sub store {
502
    my ($self, $object_or_list) = @_;
503
504
505
506

    my $objects = (ref($object_or_list) eq 'ARRAY')     # ensure we get an array of objects to store
        ? $object_or_list
        : [ $object_or_list ];
507
    return ([], 0) unless(scalar(@$objects));
508

509
510
    my $table_name              = $self->table_name();
    my $autoinc_id              = $self->autoinc_id();
511
    my $all_storable_columns    = [ grep { $_ ne $autoinc_id } keys %{ $self->column_set() } ];
512
513
514
    my $driver                  = $self->dbc->driver();
    my $insertion_method        = $self->insertion_method;  # INSERT, INSERT_IGNORE or REPLACE
    $insertion_method           =~ s/_/ /g;
515
516
    if($driver eq 'sqlite') {
        $insertion_method =~ s/INSERT IGNORE/INSERT OR IGNORE/ig;
517
518
    } elsif($driver eq 'pgsql') {   # FIXME! temporary hack
        $insertion_method = 'INSERT';
519
    }
520

521
    my %hashed_sth = ();  # do not prepare statements until there is a real need
522

523
524
    my $stored_this_time        = 0;

525
    foreach my $object (@$objects) {
526
            my ($columns_being_stored, $column_key) = $self->keys_to_columns($object);
527
            # warn "COLUMN_KEY='$column_key'\n";
528
529

            my $this_sth;
530

531
532
533
534
                # only prepare (once!) if we get here:
            unless($this_sth = $hashed_sth{$column_key}) {
                    # By using question marks we can insert true NULLs by setting corresponding values to undefs:
                my $sql = "$insertion_method INTO $table_name (".join(', ', @$columns_being_stored).') VALUES ('.join(',', (('?') x scalar(@$columns_being_stored))).')';
535
                # warn "STORE: $sql\n";
536
537
538
                $this_sth = $hashed_sth{$column_key} = $self->prepare( $sql ) or die "Could not prepare statement: $sql";
            }

539
            # warn "STORED_COLUMNS: ".stringify($columns_being_stored)."\n";
540
            my $values_being_stored = $self->slicer( $object, $columns_being_stored );
541
            # warn "STORED_VALUES: ".stringify($values_being_stored)."\n";
542

543
            my $return_code = $this_sth->execute( @$values_being_stored )
544
                    # using $return_code in boolean context allows to skip the value '0E0' ('no rows affected') that Perl treats as zero but regards as true:
545
                or die "Could not store fields\n\t{$column_key}\nwith data:\n\t(".join(',', @$values_being_stored).')';
Leo Gordon's avatar
Leo Gordon committed
546
            if($return_code > 0) {     # <--- for the same reason we have to be explicitly numeric here
547
548
                my $liid = $autoinc_id && $self->dbc->db_handle->last_insert_id(undef, undef, $table_name, $autoinc_id);
                $self->mark_stored($object, $liid );
549
                ++$stored_this_time;
550
551
552
            }
    }

553
554
555
    foreach my $sth (values %hashed_sth) {
        $sth->finish();
    }
556

557
    return ($object_or_list, $stored_this_time);
558
559
560
561
562
563
564
565
}


sub DESTROY { }   # to simplify AUTOLOAD

sub AUTOLOAD {
    our $AUTOLOAD;

566
567
568
569
570
    if($AUTOLOAD =~ /::fetch(_all)?(?:_by_(\w+?))?(?:_HASHED_FROM_(\w+?))?(?:_TO_(\w+?))?$/) {
        my $all             = $1;
        my $filter_string   = $2;
        my $key_string      = $3;
        my $value_column    = $4;
571
572
573
574

        my ($self) = @_;
        my $column_set = $self->column_set();

575
        my $filter_components = $filter_string && [ split(/_AND_/i, $filter_string) ];
576
577
578
579
580
        if($filter_components) {
            foreach my $column_name ( @$filter_components ) {
                unless($column_set->{$column_name}) {
                    die "unknown column '$column_name'";
                }
581
582
            }
        }
583

584
        my $key_components = $key_string && [ split(/_AND_/i, $key_string) ];
585
586
587
588
589
        if($key_components) {
            foreach my $column_name ( @$key_components ) {
                unless($column_set->{$column_name}) {
                    die "unknown column '$column_name'";
                }
590
591
            }
        }
592

593
594
595
        if($value_column && !$column_set->{$value_column}) {
            die "unknown column '$value_column'";
        }
596

597
#        warn "Setting up '$AUTOLOAD' method\n";
598
599
600
        *$AUTOLOAD = sub {
            my $self = shift @_;
            return $self->fetch_all(
601
                $filter_components && join(' AND ', map { "$filter_components->[$_]='$_[$_]'" } 0..scalar(@$filter_components)-1),
602
603
604
605
606
                !$all,
                $key_components,
                $value_column
            );
        };
607
        goto &$AUTOLOAD;    # restart the new method
608

609
    } elsif($AUTOLOAD =~ /::count_all(?:_by_(\w+?))?(?:_HASHED_FROM_(\w+?))?$/) {
610
611
        my $filter_string   = $1;
        my $key_string      = $2;
612
613
614
615

        my ($self) = @_;
        my $column_set = $self->column_set();

616
        my $filter_components = $filter_string && [ split(/_AND_/i, $filter_string) ];
617
618
619
620
621
622
623
624
625
626
627
628
629
630
        if($filter_components) {
            foreach my $column_name ( @$filter_components ) {
                unless($column_set->{$column_name}) {
                    die "unknown column '$column_name'";
                }
            }
        }

        my $key_components = $key_string && [ split(/_AND_/i, $key_string) ];
        if($key_components) {
            foreach my $column_name ( @$key_components ) {
                unless($column_set->{$column_name}) {
                    die "unknown column '$column_name'";
                }
631
            }
632
        }
633

634
#        warn "Setting up '$AUTOLOAD' method\n";
635
636
637
        *$AUTOLOAD = sub {
            my $self = shift @_;
            return $self->count_all(
638
639
                $filter_components && join(' AND ', map { "$filter_components->[$_]='$_[$_]'" } 0..scalar(@$filter_components)-1),
                $key_components,
640
641
642
643
            );
        };
        goto &$AUTOLOAD;    # restart the new method

644
645
646
647
648
649
650
    } elsif($AUTOLOAD =~ /::remove_all_by_(\w+)$/) {
        my $filter_name = $1;

        my ($self) = @_;
        my $column_set = $self->column_set();

        if($column_set->{$filter_name}) {
651
#            warn "Setting up '$AUTOLOAD' method\n";
652
653
654
655
656
            *$AUTOLOAD = sub { my ($self, $filter_value) = @_; return $self->remove_all("$filter_name='$filter_value'"); };
            goto &$AUTOLOAD;    # restart the new method
        } else {
            die "unknown column '$filter_name'";
        }
657
    } elsif($AUTOLOAD =~ /::update_(\w+)$/) {
658
        my @columns_to_update = split(/_AND_/i, $1);
659
#        warn "Setting up '$AUTOLOAD' method\n";
660
661
662
        *$AUTOLOAD = sub { my ($self, $object) = @_; return $self->update($object, @columns_to_update); };
        goto &$AUTOLOAD;    # restart the new method
    } else {
663
        warn "sub '$AUTOLOAD' not implemented";
664
665
666
667
668
    }
}

1;