File Coverage

blib/lib/OTRS/OPM/Maker/Command/sopm.pm
Criterion Covered Total %
statement 325 350 92.8
branch 111 162 68.5
condition 19 34 55.8
subroutine 30 34 88.2
pod 5 5 100.0
total 490 585 83.7


line stmt bran cond sub pod time code
1             package OTRS::OPM::Maker::Command::sopm;
2              
3 29     29   26407 use v5.10;
  29         113  
4              
5 29     29   166 use strict;
  29         53  
  29         599  
6 29     29   137 use warnings;
  29         67  
  29         692  
7              
8             # ABSTRACT: Build .sopm file based on metadata
9              
10 29     29   151 use Carp;
  29         58  
  29         2086  
11 29     29   16191 use File::Find::Rule;
  29         249905  
  29         223  
12 29     29   1759 use File::Basename;
  29         69  
  29         1994  
13 29     29   185 use File::Spec;
  29         69  
  29         558  
14 29     29   15455 use IO::File;
  29         238557  
  29         4048  
15 29     29   19782 use JSON;
  29         242422  
  29         255  
16 29     29   4635 use List::Util qw(first);
  29         73  
  29         1805  
17 29     29   13207 use Path::Class ();
  29         770538  
  29         875  
18 29     29   20906 use XML::LibXML;
  29         1487908  
  29         223  
19 29     29   20648 use XML::LibXML::PrettyPrint;
  29         233125  
  29         317  
20              
21 29     29   19163 use OTRS::OPM::Maker -command;
  29         1290138  
  29         350  
22 29     29   184880 use OTRS::OPM::Maker::Utils::OTRS3;
  29         93  
  29         986  
23 29     29   13197 use OTRS::OPM::Maker::Utils::OTRS4;
  29         101  
  29         138283  
24              
25             our $VERSION = 1.43;
26              
27             sub abstract {
28 0     0 1 0 return "build sopm file based on metadata";
29             }
30              
31             sub usage_desc {
32 0     0 1 0 return "opmbuild sopm [--config ] [--cvs] ";
33             }
34              
35             sub opt_spec {
36             return (
37 0     0 1 0 [ 'config=s', 'JSON file that provides all the metadata' ],
38             [ 'cvs' , 'Add CVS tag to .sopm' ],
39             );
40             }
41              
42             sub validate_args {
43 0     0 1 0 my ($self, $opt, $args) = @_;
44              
45 0 0       0 if ( !$opt->{config} ) {
46 0   0     0 my @json_files = File::Find::Rule->file->name( '*.json' )->in( $args->[0] || '.' );
47              
48             @json_files > 1 ?
49             $self->usage_error( 'found more than one json file, please specify the config file to use' ) :
50 0 0       0 do{ $opt->{config} = $json_files[0] };
  0         0  
51             }
52            
53 0 0       0 if ( !$opt->{config} ) {
54 0         0 $self->usage_error( 'Please specify the config file to use' );
55             }
56            
57 0         0 my $config = Path::Class::File->new( $opt->{config} );
58 0         0 my $json = JSON->new->relaxed;
59 0         0 my $json_text = $config->slurp;
60 0 0       0 $self->usage_error( 'config file has to be in JSON format: ' . $@ ) if ! eval{ $json->decode( $json_text ); 1; };
  0         0  
  0         0  
61             }
62              
63             sub execute {
64 32     32 1 53702 my ($self, $opt, $args) = @_;
65              
66 32 50       2285 if ( !$opt->{config} ) {
67 0         0 print $self->usage->text;
68 0         0 return;
69             }
70            
71 32         319 my $config = Path::Class::File->new( $opt->{config} );
72 32         8936 my $json_text = $config->slurp;
73 32         11061 my $object = JSON->new->relaxed;
74 32         4208 my $json = $object->decode( $json_text );
75 32         2312 my $name = $json->{name};
76              
77 32 50       1737 chdir $args->[0] if $args->[0];
78              
79             # check needed info
80 32         133 for my $needed (qw(name version framework)) {
81 96 50       314 if ( !$json->{$needed} ) {
82 0         0 carp "Need $needed in config file";
83 0         0 exit 1;
84             }
85             }
86            
87 32         77 my @xml_parts;
88             my %major_versions;
89              
90             {
91 32         71 for my $framework ( @{ $json->{framework} } ) {
  32         61  
  32         93  
92 55         110 my $version = $framework;
93 55         95 my $min = '';
94 55         107 my $max = '';
95              
96 55 100       174 if ( 'HASH' eq ref $framework ) {
97 3         7 $version = $framework->{version};
98 3         4 $min = $framework->{min};
99 3         6 $max = $framework->{max};
100             }
101              
102 55 100       436 push @xml_parts, sprintf " %s",
    100          
103             ( $min ? qq~ Minimum="$min"~ : '' ),
104             ( $max ? qq~ Maximum="$max"~ : '' ),
105             $version;
106              
107 55         187 my $major_version = (split /\./, $version)[0];
108 55         191 $major_versions{$major_version}++;
109             }
110              
111 32 100       135 if ( 2 <= keys %major_versions ) {
112 2         549 carp "Two major versions declared in framework settings. Those might be incompatible.\n";
113             }
114             }
115              
116 32         364 my %utils_versions = (
117             '3' => 'OTRS3',
118             '4' => 'OTRS4',
119             '5' => 'OTRS4',
120             '6' => 'OTRS4',
121             );
122              
123 32         144 my ($max) = sort{ $b <=> $a }keys %major_versions;
  2         12  
124              
125 32   100     279 $json->{product} //= 'OTRS';
126 32 100       178 if ( uc $json->{product} eq 'KIX' ) {
127 1         3 $max = 5;
128             }
129              
130 32   33     111 my $mod = $utils_versions{$max} || $utils_versions{3};
131 32         95 my $utils = 'OTRS::OPM::Maker::Utils::' . $mod;
132              
133 32 100       102 if ( $json->{requires} ) {
134             {
135 14         26 for my $name ( sort keys %{ $json->{requires}->{package} } ) {
  14         52  
136 14         71 push @xml_parts, sprintf ' %s', $json->{requires}->{package}->{$name}, $name;
137             }
138             }
139            
140             {
141 14         63 for my $name ( sort keys %{ $json->{requires}->{module} } ) {
  14         35  
  14         24  
  14         44  
142 14         56 push @xml_parts, sprintf ' %s', $json->{requires}->{module}->{$name}, $name;
143             }
144             }
145             }
146              
147 32   50     160 push @xml_parts, sprintf " %s", $json->{vendor}->{name} || '';
148 32   50     149 push @xml_parts, sprintf " %s", $json->{vendor}->{url} || '';
149              
150 32 50       109 if ( $json->{description} ) {
151 32         84 for my $lang ( sort keys %{ $json->{description} } ) {
  32         104  
152 32         166 push @xml_parts, sprintf ' %s', $lang, $json->{description}->{$lang};
153             }
154             }
155              
156 32 50       113 if ( $json->{license} ) {
157 32         127 push @xml_parts, sprintf ' %s', $json->{license};
158             }
159              
160             # create filelist
161             {
162 32         59 my @files = File::Find::Rule->file->in( '.' );
  32         1213  
163              
164             # remove "hidden" files from list; and do not list .sopm
165             @files = grep{
166 32         32159 ( substr( $_, 0, 1 ) ne '.' ) &&
167             $_ !~ m{[\\/]\.} &&
168 154 50 33     1119 $_ ne $json->{name} . '.sopm'
169             }sort @files;
170              
171 32 100 66     136 if ( $json->{exclude_files} and 'ARRAY' eq ref $json->{exclude_files} ) {
172 1         3 for my $index ( reverse 0 .. $#files ) {
173 2         4 my $file = $files[$index];
174             my $excluded = first {
175 2     2   5 eval{ $file =~ /$_\z/ };
  2         19  
176 2         7 }@{ $json->{exclude_files} };
  2         7  
177              
178 2 100       14 splice @files, $index, 1 if $excluded;
179             }
180             }
181              
182 32         288 $utils->filecheck( \@files );
183              
184             push @xml_parts,
185             sprintf " \n%s\n ",
186 32 50       82 join "\n", map{ my $permission = $_ =~ /^bin/ ? 755 : 644; qq~ ~ }@files;
  152         358  
  152         596  
187             }
188              
189 32 100 66     150 if ( $json->{changes_file} && -f $config->dir . "/" . $json->{changes_file} ) {
190 1         57 my $changes_file = Path::Class::File->new( $config->dir, $json->{changes_file} );
191 1         109 my $lines = $changes_file->slurp( iomode => '<:encoding(UTF-8)' );
192              
193 1   50     1503 my @entries = grep{ ( $_ // '' ) ne '' }split m{
  5         18  
194             (?:\s+)?
195             ( # headline with version and date
196             ^
197             \d+\.\d+ (?:\.\d+)? # version
198             \s+ - \s+
199             \d{4}-\d{2}-\d{2} \s # date
200             \d{2}:\d{2}:\d{2} # time
201             )
202             \s+
203             }xms, $lines;
204              
205 1         4 while ( @entries ) {
206 2         6 my ($header, $desc) = ( shift(@entries), shift(@entries) );
207              
208 2   50     14 my ($version, $date) = split /\s+-\s+/, $header // '';
209              
210 2         10 $desc =~ s{\s+\z}{};
211              
212 2         29 push @xml_parts, sprintf qq~ ~, $version, $date, $desc;
213             }
214             }
215              
216             # changelog
217             {
218 32         64 CHANGE:
219 32 100       62 for my $change ( @{ $json->{changes} || [] } ) {
  32         185  
220 2         4 my $version = '';
221 2         3 my $date = '';
222 2         10 my $info = '';
223              
224 2 100       6 if ( !ref $change ) {
    50          
225 1         2 $info = $change;
226             }
227             elsif ( 'HASH' eq ref $change ) {
228 1         3 $info = $change->{message};
229 1 50       6 $version = sprintf( ' Version="%s"', $change->{version} ) if $change->{version};
230 1 50       3 $date = sprintf( ' Date="%s"', $change->{date} ) if $change->{date};
231             }
232              
233 2 50       4 next CHANGE if !length $info;
234              
235 2         9 push @xml_parts, sprintf " %s", $version, $date, $info;
236             }
237             }
238              
239 32         182 my %actions = (
240             Install => 'post',
241             Uninstall => 'pre',
242             Upgrade => 'post',
243             );
244              
245 32         237 my %action_code = (
246             TableCreate => \&_TableCreate,
247             Insert => \&_Insert,
248             TableDrop => \&_TableDrop,
249             ColumnAdd => \&_ColumnAdd,
250             ColumnDrop => \&_ColumnDrop,
251             ColumnChange => \&_ColumnChange,
252             ForeignKeyCreate => \&_ForeignKeyCreate,
253             ForeignKeyDrop => \&_ForeignKeyDrop,
254             UniqueDrop => \&_UniqueDrop,
255             UniqueCreate => \&_UniqueCreate,
256             );
257            
258 32         133 my %tables_to_delete;
259             my %own_tables;
260 32         0 my @columns_to_delete;
261 32         0 my %db_actions;
262              
263 32         61 my $table_counter = 0;
264 32         53 my $column_counter;
265              
266             ACTION:
267 32 100       54 for my $action ( @{ $json->{database} || [] } ) {
  32         163  
268 27         61 my $tmp_version = $action->{version};
269 27 100       80 my @versions = ref $tmp_version ? @{$tmp_version} : ($tmp_version);
  5         13  
270              
271             VERSION:
272 27         60 for my $version ( @versions ) {
273 32 100       80 my $action_type = $version ? 'Upgrade' : 'Install';
274 32         55 my $op = $action->{type};
275              
276 32 100       76 if ( $action->{uninstall} ) {
277 1         2 $action_type = 'Uninstall';
278             }
279              
280 32 50       103 next VERSION if !$action_code{$op};
281            
282 32 100       102 if ( $op eq 'TableCreate' ) {
    50          
283 13         29 my $table = $action->{name};
284 13         74 $tables_to_delete{$table} = $table_counter++;
285 13         29 $own_tables{$table} = 1;
286             }
287             elsif ( $op eq 'TableDrop' ) {
288 0         0 my $table = $action->{name};
289 0         0 delete $tables_to_delete{$table};
290             }
291              
292 32 100       76 if ( $op eq 'ColumnAdd' ) {
293 3         19 my $table = $action->{name};
294 3 100       8 if ( !$own_tables{$table} ) {
295             unshift @columns_to_delete, +{
296             name => $table,
297 2 50       5 columns => [ map { $_->{name} } @{ $action->{columns} || [] } ],
  2         9  
  2         7  
298             };
299             }
300             }
301            
302 32         145 $action->{version} = $version;
303 32         50 push @{ $db_actions{$action_type} }, $action_code{$op}->($action);
  32         101  
304             }
305             }
306            
307 29         68 for my $columns_delete ( @columns_to_delete ) {
308 1         2 push @{ $db_actions{Uninstall} }, _ColumnDrop($columns_delete);
  1         3  
309             }
310              
311 29 100       112 if ( %tables_to_delete ) {
312 10         43 for my $table ( sort { $tables_to_delete{$b} <=> $tables_to_delete{$a} }keys %tables_to_delete ) {
  2         8  
313 12         22 push @{ $db_actions{Uninstall} }, _TableDrop({ name => $table });
  12         44  
314             }
315             }
316              
317 29         69 for my $action_type ( qw/Install Upgrade Uninstall/ ) {
318            
319 87 100       247 next if !$db_actions{$action_type};
320            
321 29         55 my $order = $actions{$action_type};
322            
323             push @xml_parts,
324             sprintf qq~
325             %s
326 29         109 ~, join "\n", @{ $db_actions{$action_type} };
  29         210  
327             }
328            
329 29 100       59 for my $code ( @{ $json->{code} || [] } ) {
  29         155  
330 27 100       85 if ( !ref $code ) {
331 4 100       18 $code = {
332             type => $code,
333             version => 0,
334             phase => ( $code eq 'Uninstall' ? 'pre' : 'post' ),
335             };
336             }
337              
338 27         76 $code->{type} = 'Code' . $code->{type};
339             push @xml_parts, $utils->packagesetup(
340             $code->{type},
341             $code->{version},
342             $code->{function} || $code->{type},
343             $code->{phase},
344             $code->{package},
345 27   66     186 );
346             }
347              
348 29 100       66 for my $intro ( @{ $json->{intro} || [] } ) {
  29         180  
349 2         33 push @xml_parts, _IntroTemplate( $intro );
350             }
351              
352 29         97 my $cvs = "";
353 29 100       126 if ( $opt->{cvs} ) {
354 1         3 $cvs = sprintf qq~\n \$Id: %s.sopm,v 1.1.1.1 2011/04/15 07:49:58 rb Exp \$~, $name;
355             }
356            
357             my $xml = sprintf q~
358            
359             %s
360             %s
361             %s
362             %s
363            
364             ~,
365             $VERSION,
366             $cvs,
367             $name,
368             $json->{version},
369 29         674 join( "\n", @xml_parts );
370              
371 29 50       242 my $fh = IO::File->new( $name . '.sopm', 'w' ) or die $!;
372 29         5232 $fh->print( $xml );
373 29         829 $fh->close;
374             }
375              
376             sub _IntroTemplate {
377 2     2   6 my ($intro) = @_;
378              
379 2 50       5 my $version = $intro->{version} ? ' Version="' . $intro->{version} . '"' : '';
380 2         3 my $type = $intro->{type};
381 2 100       6 my $text = ref $intro->{text} ? join( "
\n", @{ $intro->{text} } ) : $intro->{text};
  1         3  
382 2   100     31 my $phase = $intro->{time} || "post";
383 2 100       8 my $lang = $intro->{lang} ? ' Lang="' . $intro->{lang} . '"' : '';
384 2 100       6 my $title = $intro->{title} ? ' Title="' . $intro->{title} . '"' : '';
385              
386 2         10 return qq~
387             $text
388             ]]>~;
389             }
390              
391             sub _Insert {
392 10     10   21 my ($action) = @_;
393              
394              
395 10         15 my $table = $action->{name};
396 10         19 my $version = $action->{version};
397              
398 10 100       25 my $version_string = $version ? ' Version="' . $version . '"' : '';
399              
400 10         24 my $string = ' \n";
401              
402             COLUMN:
403 10 50       16 for my $column ( @{ $action->{columns} || [] } ) {
  10         27  
404 70 100       205 my $value = ref $column->{value} ? join( "\n", @{ $column->{value} } ) : $column->{value};
  2         5  
405 70   50     137 $value //= '';
406              
407             $string .= sprintf ' %s' . "\n",
408             $column->{name},
409             ( $column->{type} ?
410 70 100       243 (' Type="' . $column->{type} . '"', '' ) :
411             ("", $value)
412             );
413             }
414              
415 10         21 $string .= ' ';
416              
417 10         48 return $string;
418             }
419              
420             sub _TableDrop {
421 12     12   25 my ($action) = @_;
422              
423 12         22 my $table = $action->{name};
424              
425 12         52 return ' ';
426             }
427              
428             sub _TableCreate {
429 13     13   28 my ($action) = @_;
430              
431 13         26 my $table = $action->{name};
432 13         30 my $version = $action->{version};
433              
434 13 50       39 my $version_string = $version ? ' Version="' . $version . '"' : '';
435              
436 13         43 my $string = ' \n";
437              
438             COLUMN:
439 13 50       35 for my $column ( @{ $action->{columns} || [] } ) {
  13         92  
440 37         97 my $type = _TypeCheck( $column->{type}, 'TableCreate' );
441             $string .= sprintf ' ' . "\n",
442             $column->{name},
443             $column->{required},
444             $type,
445             ( $column->{size} ? ' Size="' . $column->{size} . '"' : "" ),
446             ( $column->{auto_increment} ? ' AutoIncrement="true"' : "" ),
447 36 100       293 ( $column->{primary_key} ? ' PrimaryKey="true"' : "" ),
    100          
    100          
448             }
449              
450             UNIQUE:
451 12 100       30 for my $unique ( @{ $action->{unique} || [] } ) {
  12         144  
452 2         4 my $table = $unique->{name};
453 2   66     7 $string .= ' {columns} || ["unique$table"] } ) ) . '">' . "\n";
454              
455 2 50       3 for my $column ( @{ $unique->{columns} || [] } ) {
  2         5  
456 4         9 $string .= ' ' . "\n";
457             }
458              
459 2         4 $string .= ' ' . "\n";
460             }
461              
462             KEY:
463 12 100       26 for my $key ( @{ $action->{keys} || [] } ) {
  12         41  
464 8         15 my $table = $key->{name};
465 8         22 $string .= ' ' . "\n";
466              
467 8 50       14 for my $reference ( @{ $key->{references} || [] } ) {
  8         48  
468 8         14 my $local = $reference->{local};
469 8         14 my $foreign = $reference->{foreign};
470 8         24 $string .= ' ' . "\n";
471             }
472              
473 8         28 $string .= ' ' . "\n";
474             }
475              
476 12         26 $string .= ' ';
477              
478 12         46 return $string;
479             }
480              
481             sub _ColumnAdd {
482 3     3   7 my ($action) = @_;
483              
484 3         5 my $table = $action->{name};
485 3         5 my $version = $action->{version};
486              
487 3 50       8 my $version_string = $version ? ' Version="' . $version . '"' : '';
488              
489 3         8 my $string = ' \n";
490              
491             COLUMN:
492 3 50       5 for my $column ( @{ $action->{columns} || [] } ) {
  3         9  
493 3         6 my $type = _TypeCheck( $column->{type}, 'ColumnAdd' );
494             $string .= sprintf ' ' . "\n",
495             $column->{name},
496             $column->{required},
497             $type,
498             ( $column->{size} ? ' Size="' . $column->{size} . '"' : "" ),
499             ( $column->{auto_increment} ? ' AutoIncrement="true"' : "" ),
500 2 50       16 ( $column->{primary_key} ? ' PrimaryKey="true"' : "" ),
    50          
    50          
501             }
502              
503 2         4 $string .= ' ';
504              
505 2         7 return $string;
506             }
507              
508             sub _ColumnDrop {
509 2     2   4 my ($action) = @_;
510              
511 2         4 my $table = $action->{name};
512 2         3 my $version = $action->{version};
513              
514 2 50       5 my $version_string = $version ? ' Version="' . $version . '"' : '';
515              
516 2         6 my $string = ' \n";
517              
518             COLUMN:
519 2 50       3 for my $column ( @{ $action->{columns} || [] } ) {
  2         6  
520 2         8 $string .= sprintf qq~ \n~, $column;
521             }
522              
523 2         5 $string .= ' ';
524              
525 2         7 return $string;
526             }
527              
528             sub _ForeignKeyCreate {
529 1     1   3 my ($action) = @_;
530              
531 1         11 my $table = $action->{name};
532 1         2 my $version = $action->{version};
533              
534 1 50       4 my $version_string = $version ? ' Version="' . $version . '"' : '';
535              
536 1         3 my $string = ' \n";
537              
538             COLUMN:
539 1 50       1 for my $reference ( @{ $action->{references} || [] } ) {
  1         4  
540             $string .= sprintf '
541            
542             ' . "\n",
543             $reference->{name},
544             $reference->{local},
545 2         11 $reference->{foreign};
546             }
547              
548 1         3 $string .= ' ';
549              
550 1         3 return $string;
551             }
552              
553             sub _ForeignKeyDrop {
554 1     1   2 my ($action) = @_;
555              
556 1         11 my $table = $action->{name};
557 1         2 my $version = $action->{version};
558              
559 1 50       3 my $version_string = $version ? ' Version="' . $version . '"' : '';
560              
561 1         3 my $string = ' \n";
562              
563             COLUMN:
564 1 50       2 for my $reference ( @{ $action->{references} || [] } ) {
  1         3  
565             $string .= sprintf '
566            
567             ' . "\n",
568             $reference->{name},
569             $reference->{local},
570 2         10 $reference->{foreign};
571             }
572              
573 1         2 $string .= ' ';
574              
575 1         4 return $string;
576             }
577              
578             sub _UniqueCreate {
579 1     1   2 my ($action) = @_;
580              
581 1         28 my $table = $action->{name};
582 1         5 my $version = $action->{version};
583              
584 1 50       4 my $version_string = $version ? ' Version="' . $version . '"' : '';
585              
586 1         3 my $string = ' \n";
587 1         5 $string .= sprintf qq~ \n~, $action->{unique_name};
588              
589             COLUMN:
590 1 50       2 for my $column ( @{ $action->{columns} || [] } ) {
  1         3  
591 2         52 $string .= sprintf qq~ \n~,
592             $column;
593             }
594              
595 1         2 $string .= qq~ \n~;
596 1         2 $string .= ' ';
597              
598 1         6 return $string;
599             }
600              
601             sub _UniqueDrop {
602 1     1   3 my ($action) = @_;
603              
604 1         25 my $table = $action->{name};
605 1         5 my $version = $action->{version};
606              
607 1 50       4 my $version_string = $version ? ' Version="' . $version . '"' : '';
608              
609 1         3 my $string = ' \n";
610              
611             $string .= sprintf qq~ \n~,
612 1         5 $action->{unique_name};
613              
614 1         2 $string .= ' ';
615              
616 1         5 return $string;
617             }
618              
619             sub _ColumnChange {
620 1     1   2 my ($action) = @_;
621              
622 1         2 my $table = $action->{name};
623 1         2 my $version = $action->{version};
624              
625 1 50       18 my $version_string = $version ? ' Version="' . $version . '"' : '';
626              
627 1         4 my $string = ' \n";
628              
629             COLUMN:
630 1 50       2 for my $column ( @{ $action->{columns} || [] } ) {
  1         3  
631 1         4 my $type = _TypeCheck( $column->{type}, 'ColumnChange' );
632             $string .= sprintf ' ' . "\n",
633             $column->{new_name},
634             $column->{old_name},
635             $column->{required},
636             $type,
637             ( $column->{size} ? ' Size="' . $column->{size} . '"' : "" ),
638             ( $column->{auto_increment} ? ' AutoIncrement="true"' : "" ),
639 0 0       0 ( $column->{primary_key} ? ' PrimaryKey="true"' : "" ),
    0          
    0          
640             }
641              
642 0         0 $string .= ' ';
643              
644 0         0 return $string;
645             }
646              
647             sub _TypeCheck {
648 41     41   116 my ($type, $action) = @_;
649              
650 41         188 my %types = (
651             DATE => 1,
652             SMALLINT => 1,
653             BIGINT => 1,
654             INTEGER => 1,
655             DECIMAL => 1,
656             VARCHAR => 1,
657             LONGBLOB => 1,
658             );
659              
660 41 100       101 if ( !$types{$type} ) {
661 3         67 croak "$type is not allowed in $action. Allowed types: ", join ', ', sort keys %types;
662             }
663              
664 38         126 return $type;
665             }
666              
667             1;
668              
669             __END__