File Coverage

blib/lib/Clarity/XOG/Merge.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Clarity::XOG::Merge;
2              
3 3     3   32906 use 5.008;
  3         13  
  3         166  
4 3     3   22 use strict;
  3         6  
  3         108  
5 3     3   19 use warnings;
  3         27  
  3         128  
6              
7 3     3   5833 use File::Temp qw(tempfile tempdir);
  3         137776  
  3         322  
8 3     3   5258 use Data::Dumper;
  3         38035  
  3         405  
9 3     3   5463 use XML::Twig;
  0            
  0            
10             use Moose;
11              
12             our $VERSION = '1.001';
13              
14             has files => ( is => "rw", isa => "ArrayRef", default => sub {[]}, auto_deref => 1 );
15             has projectids => ( is => "rw", isa => "HashRef", default => sub {{}} );
16             has finalprojectids => ( is => "rw", isa => "HashRef", default => sub {{}} );
17             has buckets => ( is => "rw" );
18             has cur_file => ( is => "rw" );
19             has cur_proj => ( is => "rw" );
20             has tmpdir => ( is => "rw", default => sub { tempdir( CLEANUP => 1 ) });
21             has out_file => ( is => "rw", default => "XOGMERGE.xml" );
22             has ALWAYSBUCKETS => ( is => "rw", default => 1 );
23             has verbose => ( is => "rw", default => 0 );
24             has debug => ( is => "rw", default => 0 );
25             has force => ( is => "rw", default => 0 );
26              
27             sub TEMPLATE_HEADER {
28             q#<!-- edited with Emacs 23 (http://emacswiki.org) by cris (na) -->
29             <!--XOG XML from CA is prj_projects_alloc_act_etc_read. Created by xogtool -->
30             <NikuDataBus xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/nikuxog_project.xsd">
31             <Header action="write" externalSource="NIKU" objectType="project" version="7.5.0" />
32             <Projects>
33             #
34             }
35              
36             sub TEMPLATE_FOOTER {
37             q#
38             </Projects>
39             </NikuDataBus>
40             #
41             }
42              
43             sub cb_Collect_Project
44             {
45             my ($t, $project) = @_;
46             my $self = $t->{_self};
47              
48             my $projectID = $project->att('projectID');
49             my $name = $project->att('name');
50              
51             $self->projectids->{$projectID}{files}{$self->cur_file}++;
52             }
53              
54             sub prepare {
55             my ($self) = @_;
56             # prepare temp dirs
57             # open FINAL
58             }
59              
60             sub finish
61             {
62             my ($self) = @_;
63             # close FINAL;
64             # cleanup temp dirs
65             }
66              
67             sub pass1_count
68             {
69             my ($self) = @_;
70             print "Pass 1: count\n" if $self->verbose;
71             foreach my $f ($self->files) {
72             print " Read file $f\n" if $self->verbose;
73             $self->cur_file( $f );
74             my $twig= XML::Twig->new
75             ( twig_handlers =>
76             { 'Projects/Project' => \&cb_Collect_Project } );
77             $twig->{_self} = $self;
78             $twig->parsefile( $f );
79             }
80             }
81              
82             sub add_project_to_final
83             {
84             my ($self, $project) = @_;
85             # my $projectID = $project->att('projectID');
86             # my $name = $project->att('name');
87              
88             $project->set_pretty_print( 'indented'); # \n before tags not part of mixed content
89             $project->print(\*XOGMERGEOUT);
90             }
91              
92             sub add_project_to_bucket
93             {
94             my ($self, $project) = @_;
95             my $projectID = $project->att('projectID');
96             my $bucketfile = $self->tmpdir."/bucket-$projectID.tmp";
97              
98             open XOGMERGEBUCKET, ">>", $bucketfile or die "Cannot open bucket file ".$bucketfile.": $!";
99             print XOGMERGEBUCKET "<Projects>\n" if not $self->buckets->{$bucketfile};
100             $self->buckets->{$bucketfile}++;
101             $project->print(\*XOGMERGEBUCKET);
102             close XOGMERGEBUCKET;
103             }
104              
105             sub prepare_output
106             {
107             my ($self) = @_;
108              
109             my $answer = 'n';
110              
111             if (-e $self->out_file and not $self->force) {
112             print "Output file '".$self->out_file."' exists. Overwrite (y/N)? ";
113             read STDIN, $answer, 1;
114             if ($answer !~ m/^y/i) {
115             print "Keep ".$self->out_file." and exit.\n" if $self->verbose;
116             exit 1;
117             }
118             print "Ok, overwrite ".$self->out_file.".\n" if $self->verbose;
119             }
120             open XOGMERGEOUT, ">", $self->out_file or die "Cannot open out file ".$self->out_file.": $!";
121             print XOGMERGEOUT TEMPLATE_HEADER;
122             }
123              
124             sub clean_old_buckets {
125             my ($self) = @_;
126             #system ("rm -f bucket-*.tmp");
127             $self->buckets({});
128             }
129              
130             sub finish_output
131             {
132             my ($self) = @_;
133             print XOGMERGEOUT TEMPLATE_FOOTER;
134             close XOGMERGEOUT;
135             print "Out file: ".$self->out_file."\n" if $self->verbose;
136             }
137              
138             sub cb_Save_Project
139             {
140             my ($t, $project) = @_;
141             my $self = $t->{_self};
142              
143             my $projectID = $project->att('projectID');
144             my $name = $project->att('name');
145              
146             if ($self->ALWAYSBUCKETS or keys %{$self->projectids->{$projectID}{files}} > 1)
147             {
148             # do this always (without surrounding if/else
149             # if single-org-projects rarely occur
150             $self->add_project_to_bucket($project);
151             }
152             else
153             {
154             $self->add_project_to_final($project);
155             }
156             }
157              
158             sub cb_Open_Project
159             {
160             my ($t, $project) = @_;
161             my $self = $t->{_self};
162              
163             # debug
164             my $projectID = $project->att('projectID');
165             my $name = $project->att('name');
166              
167             $self->cur_proj( $project ) unless $self->cur_proj;
168             }
169              
170             sub cb_Save_Resource
171             {
172             my ($t, $resource) = @_;
173             my $self = $t->{_self};
174              
175             my $resourceID = $resource->att('resourceID');
176              
177             my $resources = $self->cur_proj->first_child('Resources');
178             my $res = $resource->cut;
179             $res->paste(last_child => $resources); # ok
180             }
181              
182             sub add_buckets_to_final
183             {
184             my ($self) = @_;
185             foreach my $bucket (keys %{$self->buckets})
186             {
187             $self->cur_file( $bucket );
188             $self->cur_proj( undef );
189             my $twig= XML::Twig->new (
190             start_tag_handlers => { "Project" => \&cb_Open_Project },
191             twig_handlers => { "Resource" => \&cb_Save_Resource },
192             );
193             $twig->{_self} = $self;
194             $twig->parsefile( $bucket );
195             $self->add_project_to_final($self->cur_proj); # wrong duplicate
196             }
197             }
198              
199             sub close_buckets_xml {
200             my ($self) = @_;
201             foreach my $bucketfile (keys %{$self->buckets}) {
202             open XOGMERGEBUCKET, ">>", $bucketfile or die "Cannot open bucket file ".$bucketfile.": $!";
203             print XOGMERGEBUCKET "</Projects>\n";
204             close XOGMERGEBUCKET;
205             }
206             }
207              
208             sub collect_projects_to_buckets_or_final
209             {
210             my ($self) = @_;
211             foreach my $f ($self->files)
212             {
213             print " Read file $f\n" if $self->verbose;
214             $self->cur_file( $f );
215             my $twig= XML::Twig->new (twig_handlers => { 'Projects/Project' => \&cb_Save_Project });
216             $twig->{_self} = $self;
217             $twig->parsefile( $f );
218             }
219             $self->close_buckets_xml;
220             }
221              
222             sub pass2_merge
223             {
224             my ($self) = @_;
225             print "Pass 2: merge\n" if $self->verbose;
226             $self->prepare_output;
227             $self->clean_old_buckets;
228             $self->collect_projects_to_buckets_or_final;
229             $self->add_buckets_to_final;
230             $self->finish_output;
231             }
232              
233             sub cb_Count_Project {
234             my ($t, $project) = @_;
235             my $self = $t->{_self};
236              
237             my $projectID = $project->att('projectID');
238             my $name = $project->att('name');
239              
240             $self->finalprojectids->{$projectID}++;
241             }
242              
243             sub pass3_validate {
244             my ($self) = @_;
245              
246             print "Pass 3: validate\n" if $self->verbose;
247             print " File ".$self->out_file."\n" if $self->verbose;
248             my $twig= XML::Twig->new (twig_handlers => { 'Projects/Project' => \&cb_Count_Project });
249             $twig->{_self} = $self;
250             $twig->parsefile( $self->out_file );
251              
252             my $projectcount_in = scalar keys %{$self->projectids};
253             my $projectcount_out = scalar keys %{$self->finalprojectids};
254             if ($projectcount_in == $projectcount_out) {
255             print " OK - project count ($projectcount_in/$projectcount_out)\n" if $self->verbose;
256             } else {
257             print " NOT OK - project count ($projectcount_in/$projectcount_out)\n" if $self->verbose;
258             }
259              
260             foreach (keys %{$self->projectids}) {
261             if (exists $self->projectids->{$_}) {
262             print " OK - project $_\n" if $self->verbose;
263             } else {
264             print " NOT OK - project $_\n" if $self->verbose;
265             exit 2;
266             }
267             }
268              
269             }
270              
271             sub Main
272             {
273             my ($self) = @_;
274             $self->prepare;
275             $self->pass1_count;
276             $self->pass2_merge;
277             $self->pass3_validate;
278             $self->finish();
279             }
280              
281             1; # End of Clarity::XOG::Merge
282              
283             __END__
284              
285             =pod
286              
287             =head1 NAME
288              
289             Clarity::XOG::Merge - Merge several Clarity XML Open Gateway (XOG) files
290              
291             =head1 SYNOPSIS
292              
293             Merge several Clarity XOG ("XML Open Gateway") files into one
294              
295             use Clarity::XOG::Merge;
296             my $merger = Clarity::XOG::Merge->new
297             ( files => ['t/QA.xml', 't/PS.xml', 't/TJ.xml'],
298             out_file => $out_file );
299             $merger->Main;
300              
301             Or using the frontend tool:
302              
303             xogtool merge -i subdir_with_inputfiles -o MERGEDRESULT.xml
304              
305             =head1 ABOUT
306              
307             I<Clarity>(R) is a project and resource management software from
308             I<Computer Associates International, Inc.>(R), see
309             L<http://de.wikipedia.org/wiki/Clarity>.
310              
311             It provides data import of so called "XOG" ("XML Open Gateway") files,
312             sometimes historically called "nikureport". Such files are generated
313             for instance by TaskJuggler, see
314             L<http://www.taskjuggler.org/tj3/manual/nikureport.html>.
315              
316             Sometimes, e.g., when different departments of one company use their
317             own project management software, respectively, such XOG files need to
318             be merged into one before being imported into the central Clarity
319             database.
320              
321             This module L<Clarity::XOG::Merge|Clarity::XOG::Merge> and its
322             frontend tool B<xogtool>) provide that merging.
323              
324             It is implemented carefully to handle very large files without
325             suffering from memory issues (by using L<XML::Twig|XML::Twig>, temp
326             files and doing several passes). So the only restrictions should be the
327             supported max-file-size of your filesystem. (However, please note that
328             importing a large merged XOG file into Clarity can have its own memory
329             issues, due to their import probably being XML-DOM based.)
330              
331             It is also explicitely polished to work under Windows using
332             L<Strawberry Perl|http://strawberryperl.com/> and being packaged into
333             a single standalone C<xogtool.exe> using
334             L<PAR|http://search.cpan.org/dist/PAR> (tested with Strawberry Perl
335             5.8 on Windows XP). A corresponding C<mkexe.bat> file to call the PAR
336             packager is provided.
337              
338             =head1 AUTHOR
339              
340             Steffen Schwigon, C<< <ss5 at renormalist.net> >>
341              
342             =head1 BUGS
343              
344             Please report any bugs or feature requests to C<bug-clarity-xog-merge
345             at rt.cpan.org>, or through the web interface at
346             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Clarity-XOG-Merge>. I
347             will be notified, and then you'll automatically be notified of
348             progress on your bug as I make changes.
349              
350             =head1 COPYRIGHT & LICENSE
351              
352             Copyright 2010-2011 Steffen Schwigon, all rights reserved.
353              
354             This program is free software; you can redistribute it and/or modify
355             it under the same terms as Perl itself.
356              
357             =cut