Commit | Line | Data |
18fca96a |
1 | package DBIx::Class::Schema::Loader::Generic; |
a78e3fed |
2 | |
3 | use strict; |
a4a19f3c |
4 | use warnings; |
457eb8a6 |
5 | use base qw/Class::Accessor::Fast/; |
2a4b8262 |
6 | use Class::C3; |
a78e3fed |
7 | use Carp; |
8 | use Lingua::EN::Inflect; |
8563178e |
9 | use UNIVERSAL::require; |
f3be6194 |
10 | require DBIx::Class; |
a4a19f3c |
11 | |
3980d69c |
12 | # The first group are all arguments which are may be defaulted within, |
13 | # The last two (classes, monikers) are generated locally: |
14 | |
15 | __PACKAGE__->mk_ro_accessors(qw/ |
16 | schema |
1031d4f6 |
17 | connect_info |
3980d69c |
18 | exclude |
19 | constraint |
20 | additional_classes |
21 | additional_base_classes |
22 | left_base_classes |
9fa99683 |
23 | components |
24 | resultset_components |
3980d69c |
25 | relationships |
4350370d |
26 | inflect_map |
27 | moniker_map |
3980d69c |
28 | db_schema |
29 | drop_db_schema |
30 | debug |
31 | |
ac5ad557 |
32 | _tables |
3980d69c |
33 | classes |
34 | monikers |
35 | /); |
a4a19f3c |
36 | |
a78e3fed |
37 | =head1 NAME |
38 | |
18fca96a |
39 | DBIx::Class::Schema::Loader::Generic - Generic DBIx::Class::Schema::Loader Implementation. |
a78e3fed |
40 | |
41 | =head1 SYNOPSIS |
42 | |
18fca96a |
43 | See L<DBIx::Class::Schema::Loader> |
a78e3fed |
44 | |
45 | =head1 DESCRIPTION |
46 | |
457eb8a6 |
47 | This is the base class for the vendor-specific C<DBIx::Class::Schema::*> |
48 | classes, and implements the common functionality between them. |
49 | |
50 | =head1 OPTIONS |
a78e3fed |
51 | |
52 | Available constructor options are: |
53 | |
1031d4f6 |
54 | =head2 connect_info |
55 | |
56 | Identical to the connect_info arguments to C<connect> and C<connection> |
57 | that are mentioned in L<DBIx::Class::Schema>. |
58 | |
59 | An arrayref of connection information. For DBI-based Schemas, |
60 | this takes the form: |
61 | |
62 | connect_info => [ $dsn, $user, $pass, { AutoCommit => 1 } ], |
63 | |
457eb8a6 |
64 | =head2 additional_base_classes |
a78e3fed |
65 | |
66 | List of additional base classes your table classes will use. |
67 | |
457eb8a6 |
68 | =head2 left_base_classes |
a78e3fed |
69 | |
70 | List of additional base classes, that need to be leftmost. |
71 | |
457eb8a6 |
72 | =head2 additional_classes |
a78e3fed |
73 | |
74 | List of additional classes which your table classes will use. |
75 | |
c2849787 |
76 | =head2 components |
77 | |
78 | List of additional components to be loaded into your table classes. |
79 | A good example would be C<ResultSetManager>. |
80 | |
81 | =head2 resultset_components |
82 | |
83 | List of additional resultset components to be loaded into your table |
84 | classes. A good example would be C<AlwaysRS>. Component |
85 | C<ResultSetManager> will be automatically added to the above |
86 | C<components> list if this option is set. |
87 | |
457eb8a6 |
88 | =head2 constraint |
a78e3fed |
89 | |
90 | Only load tables matching regex. |
91 | |
457eb8a6 |
92 | =head2 exclude |
a78e3fed |
93 | |
94 | Exclude tables matching regex. |
95 | |
457eb8a6 |
96 | =head2 debug |
a78e3fed |
97 | |
98 | Enable debug messages. |
99 | |
457eb8a6 |
100 | =head2 relationships |
a78e3fed |
101 | |
102 | Try to automatically detect/setup has_a and has_many relationships. |
103 | |
4350370d |
104 | =head2 moniker_map |
105 | |
106 | Overrides the default tablename -> moniker translation. Can be either |
107 | a hashref of table => moniker names, or a coderef for a translator |
108 | function taking a single scalar table name argument and returning |
109 | a scalar moniker. If the hash entry does not exist, or the function |
110 | returns a false/undef value, the code falls back to default behavior |
111 | for that table name. |
112 | |
113 | =head2 inflect_map |
114 | |
115 | Just like L</moniker_map> above, but for inflecting (pluralizing) |
116 | relationship names. |
117 | |
457eb8a6 |
118 | =head2 inflect |
a78e3fed |
119 | |
4350370d |
120 | Deprecated. Equivalent to L</inflect_map>, but previously only took |
121 | a hashref argument, not a coderef. If you set C<inflect> to anything, |
122 | that setting will be copied to L</inflect_map>. |
a78e3fed |
123 | |
1031d4f6 |
124 | =head2 dsn |
125 | |
126 | DEPRECATED, use L</connect_info> instead. |
127 | |
128 | DBI Data Source Name. |
129 | |
457eb8a6 |
130 | =head2 user |
a78e3fed |
131 | |
1031d4f6 |
132 | DEPRECATED, use L</connect_info> instead. |
133 | |
a78e3fed |
134 | Username. |
135 | |
1031d4f6 |
136 | =head2 password |
137 | |
138 | DEPRECATED, use L</connect_info> instead. |
139 | |
140 | Password. |
141 | |
142 | =head2 options |
143 | |
144 | DEPRECATED, use L</connect_info> instead. |
145 | |
146 | DBI connection options hashref, like: |
147 | |
148 | { AutoCommit => 1 } |
149 | |
457eb8a6 |
150 | =head1 METHODS |
2a4b8262 |
151 | |
a78e3fed |
152 | =cut |
153 | |
3980d69c |
154 | # ensure that a peice of object data is a valid arrayref, creating |
155 | # an empty one or encapsulating whatever's there. |
156 | sub _ensure_arrayref { |
157 | my $self = shift; |
e26a4023 |
158 | |
3980d69c |
159 | foreach (@_) { |
160 | $self->{$_} ||= []; |
161 | $self->{$_} = [ $self->{$_} ] |
162 | unless ref $self->{$_} eq 'ARRAY'; |
163 | } |
a78e3fed |
164 | } |
165 | |
457eb8a6 |
166 | =head2 new |
167 | |
168 | Constructor for L<DBIx::Class::Schema::Loader::Generic>, used internally |
169 | by L<DBIx::Class::Schema::Loader>. |
170 | |
171 | =cut |
172 | |
3980d69c |
173 | sub new { |
174 | my ( $class, %args ) = @_; |
a78e3fed |
175 | |
3980d69c |
176 | my $self = { %args }; |
fbd83464 |
177 | |
3980d69c |
178 | bless $self => $class; |
fbd83464 |
179 | |
3980d69c |
180 | $self->{db_schema} ||= ''; |
181 | $self->{constraint} ||= '.*'; |
3980d69c |
182 | $self->_ensure_arrayref(qw/additional_classes |
183 | additional_base_classes |
c2849787 |
184 | left_base_classes |
9fa99683 |
185 | components |
1031d4f6 |
186 | resultset_components |
187 | connect_info/); |
c2849787 |
188 | |
189 | push(@{$self->{components}}, 'ResultSetManager') |
190 | if @{$self->{resultset_components}}; |
fbd83464 |
191 | |
3980d69c |
192 | $self->{monikers} = {}; |
193 | $self->{classes} = {}; |
a78e3fed |
194 | |
4350370d |
195 | # Support deprecated argument name |
196 | $self->{inflect_map} ||= $self->{inflect}; |
197 | |
1031d4f6 |
198 | # Support deprecated connect_info args, even mixed |
199 | # with a valid partially-filled connect_info |
200 | $self->{connect_info}->[0] ||= $self->{dsn}; |
201 | $self->{connect_info}->[1] ||= $self->{user}; |
202 | $self->{connect_info}->[2] ||= $self->{password}; |
203 | $self->{connect_info}->[3] ||= $self->{options}; |
204 | |
2a4b8262 |
205 | $self; |
206 | } |
207 | |
457eb8a6 |
208 | =head2 load |
209 | |
210 | Does the actual schema-construction work, used internally by |
211 | L<DBIx::Class::Schema::Loader> right after object construction. |
212 | |
213 | =cut |
214 | |
2a4b8262 |
215 | sub load { |
216 | my $self = shift; |
217 | |
1031d4f6 |
218 | $self->schema->connection(@{$self->connect_info}); |
a78e3fed |
219 | |
3980d69c |
220 | warn qq/\### START DBIx::Class::Schema::Loader dump ###\n/ |
221 | if $self->debug; |
a78e3fed |
222 | |
3980d69c |
223 | $self->_load_classes; |
224 | $self->_load_relationships if $self->relationships; |
d161cb49 |
225 | $self->_load_external; |
a78e3fed |
226 | |
3980d69c |
227 | warn qq/\### END DBIx::Class::Schema::Loader dump ###\n/ |
228 | if $self->debug; |
52bf3f26 |
229 | $self->schema->storage->disconnect; |
a78e3fed |
230 | |
3980d69c |
231 | $self; |
a78e3fed |
232 | } |
233 | |
d161cb49 |
234 | sub _load_external { |
235 | my $self = shift; |
236 | |
ac5ad557 |
237 | foreach my $table_class (map { $self->classes->{$_} } $self->tables) { |
d161cb49 |
238 | $table_class->require; |
239 | if($@ && $@ !~ /^Can't locate /) { |
240 | croak "Failed to load external class definition" |
241 | . "for '$table_class': $@"; |
242 | } |
243 | elsif(!$@) { |
244 | warn qq/# Loaded external class definition for '$table_class'\n/ |
245 | if $self->debug; |
246 | } |
247 | } |
248 | } |
249 | |
a78e3fed |
250 | # Overload in your driver class |
3980d69c |
251 | sub _db_classes { croak "ABSTRACT METHOD" } |
66742793 |
252 | |
16f6b6ac |
253 | # Inflect a relationship name |
3980d69c |
254 | sub _inflect_relname { |
255 | my ($self, $relname) = @_; |
708c0939 |
256 | |
4350370d |
257 | if( ref $self->{inflect_map} eq 'HASH' ) { |
258 | return $self->inflect_map->{$relname} |
259 | if exists $self->inflect_map->{$relname}; |
260 | } |
261 | elsif( ref $self->{inflect_map} eq 'CODE' ) { |
262 | my $inflected = $self->inflect_map->($relname); |
263 | return $inflected if $inflected; |
264 | } |
265 | |
3980d69c |
266 | return Lingua::EN::Inflect::PL($relname); |
16f6b6ac |
267 | } |
a78e3fed |
268 | |
16f6b6ac |
269 | # Set up a simple relation with just a local col and foreign table |
3980d69c |
270 | sub _make_simple_rel { |
271 | my ($self, $table, $other, $col) = @_; |
708c0939 |
272 | |
3980d69c |
273 | my $table_class = $self->classes->{$table}; |
274 | my $other_class = $self->classes->{$other}; |
275 | my $table_relname = $self->_inflect_relname(lc $table); |
66742793 |
276 | |
3980d69c |
277 | warn qq/\# Belongs_to relationship\n/ if $self->debug; |
16f6b6ac |
278 | warn qq/$table_class->belongs_to( '$col' => '$other_class' );\n\n/ |
3980d69c |
279 | if $self->debug; |
16f6b6ac |
280 | $table_class->belongs_to( $col => $other_class ); |
708c0939 |
281 | |
3980d69c |
282 | warn qq/\# Has_many relationship\n/ if $self->debug; |
16f6b6ac |
283 | warn qq/$other_class->has_many( '$table_relname' => '$table_class',/ |
284 | . qq/$col);\n\n/ |
3980d69c |
285 | if $self->debug; |
708c0939 |
286 | |
16f6b6ac |
287 | $other_class->has_many( $table_relname => $table_class, $col); |
288 | } |
a78e3fed |
289 | |
3980d69c |
290 | # not a class method, just a helper for cond_rel XXX |
291 | sub _stringify_hash { |
292 | my $href = shift; |
293 | |
294 | return '{ ' . |
295 | join(q{, }, map("$_ => $href->{$_}", keys %$href)) |
296 | . ' }'; |
297 | } |
298 | |
16f6b6ac |
299 | # Set up a complex relation based on a hashref condition |
3980d69c |
300 | sub _make_cond_rel { |
301 | my ( $self, $table, $other, $cond ) = @_; |
a78e3fed |
302 | |
3980d69c |
303 | my $table_class = $self->classes->{$table}; |
304 | my $other_class = $self->classes->{$other}; |
305 | my $table_relname = $self->_inflect_relname(lc $table); |
16f6b6ac |
306 | my $other_relname = lc $other; |
708c0939 |
307 | |
16f6b6ac |
308 | # for single-column case, set the relname to the column name, |
309 | # to make filter accessors work |
310 | if(scalar keys %$cond == 1) { |
311 | my ($col) = keys %$cond; |
312 | $other_relname = $cond->{$col}; |
4ce22656 |
313 | } |
16f6b6ac |
314 | |
315 | my $rev_cond = { reverse %$cond }; |
316 | |
c5de0b0c |
317 | for (keys %$rev_cond) { |
c2849787 |
318 | $rev_cond->{"foreign.$_"} = "self.".$rev_cond->{$_}; |
319 | delete $rev_cond->{$_}; |
c5de0b0c |
320 | } |
321 | |
3980d69c |
322 | my $cond_printable = _stringify_hash($cond) |
323 | if $self->debug; |
324 | my $rev_cond_printable = _stringify_hash($rev_cond) |
325 | if $self->debug; |
16f6b6ac |
326 | |
3980d69c |
327 | warn qq/\# Belongs_to relationship\n/ if $self->debug; |
16f6b6ac |
328 | |
329 | warn qq/$table_class->belongs_to( '$other_relname' => '$other_class',/ |
330 | . qq/$cond_printable);\n\n/ |
3980d69c |
331 | if $self->debug; |
16f6b6ac |
332 | |
333 | $table_class->belongs_to( $other_relname => $other_class, $cond); |
334 | |
3980d69c |
335 | warn qq/\# Has_many relationship\n/ if $self->debug; |
16f6b6ac |
336 | |
337 | warn qq/$other_class->has_many( '$table_relname' => '$table_class',/ |
338 | . qq/$rev_cond_printable);\n\n/ |
339 | . qq/);\n\n/ |
3980d69c |
340 | if $self->debug; |
16f6b6ac |
341 | |
342 | $other_class->has_many( $table_relname => $table_class, $rev_cond); |
a78e3fed |
343 | } |
344 | |
42c0680e |
345 | sub _use { |
346 | my $self = shift; |
347 | my $target = shift; |
348 | |
349 | foreach (@_) { |
350 | $_->require or croak ($_ . "->require: $@"); |
351 | eval "package $target; use $_;"; |
352 | croak "use $_: $@" if $@; |
353 | } |
354 | } |
355 | |
356 | sub _inject { |
357 | my $self = shift; |
358 | my $target = shift; |
359 | my $schema = $self->schema; |
360 | |
361 | foreach (@_) { |
362 | $_->require or croak ($_ . "->require: $@"); |
363 | $schema->inject_base($target, $_); |
364 | } |
365 | } |
366 | |
a78e3fed |
367 | # Load and setup classes |
3980d69c |
368 | sub _load_classes { |
369 | my $self = shift; |
af6c2665 |
370 | |
3980d69c |
371 | my @db_classes = $self->_db_classes(); |
372 | my $schema = $self->schema; |
a78e3fed |
373 | |
ac5ad557 |
374 | my $constraint = $self->constraint; |
375 | my $exclude = $self->exclude; |
376 | my @tables = sort grep |
377 | { /$constraint/ && (!$exclude || ! /$exclude/) } |
378 | $self->_tables_list; |
379 | |
380 | $self->{_tables} = \@tables; |
3980d69c |
381 | |
ac5ad557 |
382 | foreach my $table (@tables) { |
af6c2665 |
383 | my ($db_schema, $tbl) = split /\./, $table; |
a78e3fed |
384 | if($tbl) { |
ac5ad557 |
385 | $table = $self->drop_db_schema ? $tbl : $table; |
af6c2665 |
386 | } |
ac5ad557 |
387 | my $lc_table = lc $table; |
af6c2665 |
388 | |
3980d69c |
389 | my $table_moniker = $self->_table2moniker($db_schema, $tbl); |
390 | my $table_class = $schema . q{::} . $table_moniker; |
af6c2665 |
391 | |
09d632a1 |
392 | $self->classes->{$lc_table} = $table_class; |
393 | $self->monikers->{$lc_table} = $table_moniker; |
394 | $self->classes->{$table} = $table_class; |
395 | $self->monikers->{$table} = $table_moniker; |
396 | |
397 | no warnings 'redefine'; |
398 | local *Class::C3::reinitialize = sub { }; |
399 | use warnings; |
400 | |
9fa99683 |
401 | { no strict 'refs'; |
f3be6194 |
402 | @{"${table_class}::ISA"} = qw/DBIx::Class/; |
9fa99683 |
403 | } |
42c0680e |
404 | $self->_use ($table_class, @{$self->additional_classes}); |
9fa99683 |
405 | $self->_inject($table_class, @{$self->additional_base_classes}); |
406 | $table_class->load_components(@{$self->components}, @db_classes, 'Core'); |
407 | $table_class->load_resultset_components(@{$self->resultset_components}) |
408 | if @{$self->resultset_components}; |
42c0680e |
409 | $self->_inject($table_class, @{$self->left_base_classes}); |
09d632a1 |
410 | } |
411 | |
412 | Class::C3::reinitialize; |
413 | |
414 | foreach my $table (@tables) { |
415 | my $table_class = $self->classes->{$table}; |
416 | my $table_moniker = $self->monikers->{$table}; |
3980d69c |
417 | |
ac5ad557 |
418 | warn qq/\# Initializing table "$table" as "$table_class"\n/ |
3980d69c |
419 | if $self->debug; |
ac5ad557 |
420 | $table_class->table($table); |
af6c2665 |
421 | |
3980d69c |
422 | my ( $cols, $pks ) = $self->_table_info($table); |
a78e3fed |
423 | carp("$table has no primary key") unless @$pks; |
a4a19f3c |
424 | $table_class->add_columns(@$cols); |
425 | $table_class->set_primary_key(@$pks) if @$pks; |
af6c2665 |
426 | |
ac5ad557 |
427 | warn qq/$table_class->table('$table');\n/ if $self->debug; |
a78e3fed |
428 | my $columns = join "', '", @$cols; |
3980d69c |
429 | warn qq/$table_class->add_columns('$columns')\n/ if $self->debug; |
a78e3fed |
430 | my $primaries = join "', '", @$pks; |
3980d69c |
431 | warn qq/$table_class->set_primary_key('$primaries')\n/ |
432 | if $self->debug && @$pks; |
af6c2665 |
433 | |
3980d69c |
434 | $schema->register_class($table_moniker, $table_class); |
a78e3fed |
435 | } |
436 | } |
437 | |
457eb8a6 |
438 | =head2 tables |
3980d69c |
439 | |
8a6b44ef |
440 | Returns a sorted list of loaded tables, using the original database table |
ac5ad557 |
441 | names. |
3980d69c |
442 | |
457eb8a6 |
443 | my @tables = $schema->loader->tables; |
3980d69c |
444 | |
445 | =cut |
446 | |
447 | sub tables { |
448 | my $self = shift; |
449 | |
ac5ad557 |
450 | return @{$self->_tables}; |
3980d69c |
451 | } |
452 | |
a78e3fed |
453 | # Find and setup relationships |
3980d69c |
454 | sub _load_relationships { |
455 | my $self = shift; |
456 | |
457 | my $dbh = $self->schema->storage->dbh; |
708c0939 |
458 | my $quoter = $dbh->get_info(29) || q{"}; |
3980d69c |
459 | foreach my $table ( $self->tables ) { |
708c0939 |
460 | my $rels = {}; |
461 | my $sth = $dbh->foreign_key_info( '', |
3980d69c |
462 | $self->db_schema, '', '', '', $table ); |
708c0939 |
463 | next if !$sth; |
464 | while(my $raw_rel = $sth->fetchrow_hashref) { |
ac5ad557 |
465 | my $uk_tbl = $raw_rel->{UK_TABLE_NAME}; |
708c0939 |
466 | my $uk_col = lc $raw_rel->{UK_COLUMN_NAME}; |
467 | my $fk_col = lc $raw_rel->{FK_COLUMN_NAME}; |
ac5ad557 |
468 | my $relid = $raw_rel->{UK_NAME}; |
708c0939 |
469 | $uk_tbl =~ s/$quoter//g; |
470 | $uk_col =~ s/$quoter//g; |
471 | $fk_col =~ s/$quoter//g; |
4ce22656 |
472 | $relid =~ s/$quoter//g; |
473 | $rels->{$relid}->{tbl} = $uk_tbl; |
474 | $rels->{$relid}->{cols}->{$uk_col} = $fk_col; |
708c0939 |
475 | } |
476 | |
4ce22656 |
477 | foreach my $relid (keys %$rels) { |
478 | my $reltbl = $rels->{$relid}->{tbl}; |
479 | my $cond = $rels->{$relid}->{cols}; |
3980d69c |
480 | eval { $self->_make_cond_rel( $table, $reltbl, $cond ) }; |
708c0939 |
481 | warn qq/\# belongs_to_many failed "$@"\n\n/ |
3980d69c |
482 | if $@ && $self->debug; |
a78e3fed |
483 | } |
484 | } |
485 | } |
486 | |
65644119 |
487 | # Make a moniker from a table |
3980d69c |
488 | sub _table2moniker { |
489 | my ( $self, $db_schema, $table ) = @_; |
af6c2665 |
490 | |
af96f52e |
491 | my $db_schema_ns; |
af6c2665 |
492 | |
af96f52e |
493 | if($table) { |
494 | $db_schema = ucfirst lc $db_schema; |
3980d69c |
495 | $db_schema_ns = $db_schema if(!$self->drop_db_schema); |
af96f52e |
496 | } else { |
497 | $table = $db_schema; |
a78e3fed |
498 | } |
af6c2665 |
499 | |
4350370d |
500 | my $moniker; |
501 | |
502 | if( ref $self->moniker_map eq 'HASH' ) { |
503 | $moniker = $self->moniker_map->{$table}; |
504 | } |
505 | elsif( ref $self->moniker_map eq 'CODE' ) { |
506 | $moniker = $self->moniker_map->($table); |
507 | } |
508 | |
509 | $moniker ||= join '', map ucfirst, split /[\W_]+/, lc $table; |
510 | |
65644119 |
511 | $moniker = $db_schema_ns ? $db_schema_ns . $moniker : $moniker; |
af96f52e |
512 | |
65644119 |
513 | return $moniker; |
a78e3fed |
514 | } |
515 | |
516 | # Overload in driver class |
ac5ad557 |
517 | sub _tables_list { croak "ABSTRACT METHOD" } |
a78e3fed |
518 | |
3980d69c |
519 | sub _table_info { croak "ABSTRACT METHOD" } |
a78e3fed |
520 | |
457eb8a6 |
521 | =head2 monikers |
522 | |
523 | Returns a hashref of loaded table-to-moniker mappings for the original |
ac5ad557 |
524 | database table names. In cases where the database driver returns table |
525 | names as uppercase or mixed case, there will also be a duplicate entry |
526 | here in all lowercase. Best practice would be to use lower-case table |
527 | names when accessing this. |
457eb8a6 |
528 | |
529 | my $monikers = $schema->loader->monikers; |
530 | my $foo_tbl_moniker = $monikers->{foo_tbl}; |
531 | # -or- |
532 | my $foo_tbl_moniker = $schema->loader->monikers->{foo_tbl}; |
533 | # $foo_tbl_moniker would look like "FooTbl" |
534 | |
535 | =head2 classes |
536 | |
537 | Returns a hashref of table-to-classname mappings for the original database |
ac5ad557 |
538 | table names. Same lowercase stuff as above applies here. |
539 | |
540 | You probably shouldn't be using this for any normal or simple |
457eb8a6 |
541 | usage of your Schema. The usual way to run queries on your tables is via |
542 | C<$schema-E<gt>resultset('FooTbl')>, where C<FooTbl> is a moniker as |
543 | returned by C<monikers> above. |
544 | |
545 | my $classes = $schema->loader->classes; |
546 | my $foo_tbl_class = $classes->{foo_tbl}; |
547 | # -or- |
548 | my $foo_tbl_class = $schema->loader->classes->{foo_tbl}; |
549 | # $foo_tbl_class would look like "My::Schema::FooTbl", |
550 | # assuming the schema class is "My::Schema" |
551 | |
a78e3fed |
552 | =head1 SEE ALSO |
553 | |
18fca96a |
554 | L<DBIx::Class::Schema::Loader> |
a78e3fed |
555 | |
556 | =cut |
557 | |
558 | 1; |