Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Migrate / Importer.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Migrate::Importer;
50
51 use strict;
52 use warnings;
53
54 use Storable qw//;
55 use File::Spec;
56 use Carp qw/carp/;
57
58 sub new {
59     my $class = shift;
60     my $self = bless {}, $class;
61     $self->Init(@_);
62     return $self;
63 }
64
65 sub Init {
66     my $self = shift;
67     my %args = (
68         OriginalId  => undef,
69         Progress    => undef,
70         Statefile   => undef,
71         DumpObjects => undef,
72         HandleError => undef,
73         @_,
74     );
75
76     # Should we attempt to preserve record IDs as they are created?
77     $self->{OriginalId} = $args{OriginalId};
78
79     $self->{Progress} = $args{Progress};
80
81     $self->{HandleError} = sub { 0 };
82     $self->{HandleError} = $args{HandleError}
83         if $args{HandleError} and ref $args{HandleError} eq 'CODE';
84
85     if ($args{DumpObjects}) {
86         require Data::Dumper;
87         $self->{DumpObjects} = { map { $_ => 1 } @{$args{DumpObjects}} };
88     }
89
90     # Objects we've created
91     $self->{UIDs} = {};
92
93     # Columns we need to update when an object is later created
94     $self->{Pending} = {};
95
96     # Objects missing from the source database before serialization
97     $self->{Invalid} = [];
98
99     # What we created
100     $self->{ObjectCount} = {};
101
102     # To know what global CFs need to be unglobal'd and applied to what
103     $self->{NewQueues} = [];
104     $self->{NewCFs} = [];
105 }
106
107 sub Metadata {
108     my $self = shift;
109     return $self->{Metadata};
110 }
111
112 sub LoadMetadata {
113     my $self = shift;
114     my ($data) = @_;
115
116     return if $self->{Metadata};
117     $self->{Metadata} = $data;
118
119     die "Incompatible format version: ".$data->{Format}
120         if $data->{Format} ne "0.8";
121
122     $self->{Organization} = $data->{Organization};
123     $self->{Clone}        = $data->{Clone};
124     $self->{Incremental}  = $data->{Incremental};
125     $self->{Files}        = $data->{Files} if $data->{Final};
126 }
127
128 sub InitStream {
129     my $self = shift;
130
131     die "Stream initialized after objects have been recieved!"
132         if keys %{ $self->{UIDs} };
133
134     die "Cloning does not support importing the Original Id separately\n"
135         if $self->{OriginalId} and $self->{Clone};
136
137     die "RT already contains data; overwriting will not work\n"
138         if ($self->{Clone} and not $self->{Incremental})
139             and RT->SystemUser->Id;
140
141     # Basic facts of life, as a safety net
142     $self->Resolve( RT->System->UID => ref RT->System, RT->System->Id );
143     $self->SkipTransactions( RT->System->UID );
144
145     if ($self->{OriginalId}) {
146         # Where to shove the original ticket ID
147         my $cf = RT::CustomField->new( RT->SystemUser );
148         $cf->LoadByName( Name => $self->{OriginalId}, LookupType => RT::Ticket->CustomFieldLookupType, ObjectId => 0 );
149         unless ($cf->Id) {
150             warn "Failed to find global CF named $self->{OriginalId} -- creating one";
151             $cf->Create(
152                 Queue => 0,
153                 Name  => $self->{OriginalId},
154                 Type  => 'FreeformSingle',
155             );
156         }
157     }
158 }
159
160 sub Resolve {
161     my $self = shift;
162     my ($uid, $class, $id) = @_;
163     $self->{UIDs}{$uid} = [ $class, $id ];
164     return unless $self->{Pending}{$uid};
165
166     for my $ref (@{$self->{Pending}{$uid}}) {
167         my ($pclass, $pid) = @{ $self->Lookup( $ref->{uid} ) };
168         my $obj = $pclass->new( RT->SystemUser );
169         $obj->LoadByCols( Id => $pid );
170         $obj->__Set(
171             Field => $ref->{column},
172             Value => $id,
173         ) if defined $ref->{column};
174         $obj->__Set(
175             Field => $ref->{classcolumn},
176             Value => $class,
177         ) if defined $ref->{classcolumn};
178         $obj->__Set(
179             Field => $ref->{uri},
180             Value => $self->LookupObj($uid)->URI,
181         ) if defined $ref->{uri};
182     }
183     delete $self->{Pending}{$uid};
184 }
185
186 sub Lookup {
187     my $self = shift;
188     my ($uid) = @_;
189     unless (defined $uid) {
190         carp "Tried to lookup an undefined UID";
191         return;
192     }
193     return $self->{UIDs}{$uid};
194 }
195
196 sub LookupObj {
197     my $self = shift;
198     my ($uid) = @_;
199     my $ref = $self->Lookup( $uid );
200     return unless $ref;
201     my ($class, $id) = @{ $ref };
202
203     my $obj = $class->new( RT->SystemUser );
204     $obj->Load( $id );
205     return $obj;
206 }
207
208 sub Postpone {
209     my $self = shift;
210     my %args = (
211         for         => undef,
212         uid         => undef,
213         column      => undef,
214         classcolumn => undef,
215         uri         => undef,
216         @_,
217     );
218     my $uid = delete $args{for};
219
220     if (defined $uid) {
221         push @{$self->{Pending}{$uid}}, \%args;
222     } else {
223         push @{$self->{Invalid}}, \%args;
224     }
225 }
226
227 sub SkipTransactions {
228     my $self = shift;
229     my ($uid) = @_;
230     return if $self->{Clone};
231     $self->{SkipTransactions}{$uid} = 1;
232 }
233
234 sub ShouldSkipTransaction {
235     my $self = shift;
236     my ($uid) = @_;
237     return exists $self->{SkipTransactions}{$uid};
238 }
239
240 sub MergeValues {
241     my $self = shift;
242     my ($obj, $data) = @_;
243     for my $col (keys %{$data}) {
244         next if defined $obj->__Value($col) and length $obj->__Value($col);
245         next unless defined $data->{$col} and length $data->{$col};
246
247         if (ref $data->{$col}) {
248             my $uid = ${ $data->{$col} };
249             my $ref = $self->Lookup( $uid );
250             if ($ref) {
251                 $data->{$col} = $ref->[1];
252             } else {
253                 $self->Postpone(
254                     for => $obj->UID,
255                     uid => $uid,
256                     column => $col,
257                 );
258                 next;
259             }
260         }
261         $obj->__Set( Field => $col, Value => $data->{$col} );
262     }
263 }
264
265 sub SkipBy {
266     my $self = shift;
267     my ($column, $class, $uid, $data) = @_;
268
269     my $obj = $class->new( RT->SystemUser );
270     $obj->Load( $data->{$column} );
271     return unless $obj->Id;
272
273     $self->SkipTransactions( $uid );
274
275     $self->Resolve( $uid => $class => $obj->Id );
276     return $obj;
277 }
278
279 sub MergeBy {
280     my $self = shift;
281     my ($column, $class, $uid, $data) = @_;
282
283     my $obj = $self->SkipBy(@_);
284     return unless $obj;
285     $self->MergeValues( $obj, $data );
286     return 1;
287 }
288
289 sub Qualify {
290     my $self = shift;
291     my ($string) = @_;
292     return $string if $self->{Clone};
293     return $string if not defined $self->{Organization};
294     return $string if $self->{Organization} eq $RT::Organization;
295     return $self->{Organization}.": $string";
296 }
297
298 sub Create {
299     my $self = shift;
300     my ($class, $uid, $data) = @_;
301
302     # Use a simpler pre-inflation if we're cloning
303     if ($self->{Clone}) {
304         $class->RT::Record::PreInflate( $self, $uid, $data );
305     } else {
306         # Non-cloning always wants to make its own id
307         delete $data->{id};
308         return unless $class->PreInflate( $self, $uid, $data );
309     }
310
311     my $obj = $class->new( RT->SystemUser );
312     my ($id, $msg) = eval {
313         # catch and rethrow on the outside so we can provide more info
314         local $SIG{__DIE__};
315         $obj->DBIx::SearchBuilder::Record::Create(
316             %{$data}
317         );
318     };
319     if (not $id or $@) {
320         $msg ||= ''; # avoid undef
321         my $err = "Failed to create $uid: $msg $@\n" . Data::Dumper::Dumper($data) . "\n";
322         if (not $self->{HandleError}->($self, $err)) {
323             die $err;
324         } else {
325             return;
326         }
327     }
328
329     $self->{ObjectCount}{$class}++;
330     $self->Resolve( $uid => $class, $id );
331
332     # Load it back to get real values into the columns
333     $obj = $class->new( RT->SystemUser );
334     $obj->Load( $id );
335     $obj->PostInflate( $self );
336
337     return $obj;
338 }
339
340 sub ReadStream {
341     my $self = shift;
342     my ($fh) = @_;
343
344     no warnings 'redefine';
345     local *RT::Ticket::Load = sub {
346         my $self = shift;
347         my $id   = shift;
348         $self->LoadById( $id );
349         return $self->Id;
350     };
351
352     my $loaded = Storable::fd_retrieve($fh);
353
354     # Metadata is stored at the start of the stream as a hashref
355     if (ref $loaded eq "HASH") {
356         $self->LoadMetadata( $loaded );
357         $self->InitStream;
358         return;
359     }
360
361     my ($class, $uid, $data) = @{$loaded};
362
363     if ($self->{Incremental}) {
364         my $obj = $class->new( RT->SystemUser );
365         $obj->Load( $data->{id} );
366         if (not $uid) {
367             # undef $uid means "delete it"
368             $obj->Delete;
369             $self->{ObjectCount}{$class}++;
370         } elsif ( $obj->Id ) {
371             # If it exists, update it
372             $class->RT::Record::PreInflate( $self, $uid, $data );
373             $obj->__Set( Field => $_, Value => $data->{$_} )
374                 for keys %{ $data };
375             $self->{ObjectCount}{$class}++;
376         } else {
377             # Otherwise, make it
378             $obj = $self->Create( $class, $uid, $data );
379         }
380         $self->{Progress}->($obj) if $obj and $self->{Progress};
381         return;
382     } elsif ($self->{Clone}) {
383         my $obj = $self->Create( $class, $uid, $data );
384         $self->{Progress}->($obj) if $obj and $self->{Progress};
385         return;
386     }
387
388     # If it's a queue, store its ID away, as we'll need to know
389     # it to split global CFs into non-global across those
390     # fields.  We do this before inflating, so that queues which
391     # got merged still get the CFs applied
392     push @{$self->{NewQueues}}, $uid
393         if $class eq "RT::Queue";
394
395     my $origid = $data->{id};
396     my $obj = $self->Create( $class, $uid, $data );
397     return unless $obj;
398
399     # If it's a ticket, we might need to create a
400     # TicketCustomField for the previous ID
401     if ($class eq "RT::Ticket" and $self->{OriginalId}) {
402         my ($id, $msg) = $obj->AddCustomFieldValue(
403             Field             => $self->{OriginalId},
404             Value             => $self->Organization . ":$origid",
405             RecordTransaction => 0,
406         );
407         warn "Failed to add custom field to $uid: $msg"
408             unless $id;
409     }
410
411     # If it's a CF, we don't know yet if it's global (the OCF
412     # hasn't been created yet) to store away the CF for later
413     # inspection
414     push @{$self->{NewCFs}}, $uid
415         if $class eq "RT::CustomField"
416             and $obj->LookupType =~ /^RT::Queue/;
417
418     $self->{Progress}->($obj) if $self->{Progress};
419 }
420
421 sub CloseStream {
422     my $self = shift;
423
424     $self->{Progress}->(undef, 'force') if $self->{Progress};
425
426     return if $self->{Clone};
427
428     # Take global CFs which we made and make them un-global
429     my @queues = grep {$_} map {$self->LookupObj( $_ )} @{$self->{NewQueues}};
430     for my $obj (map {$self->LookupObj( $_ )} @{$self->{NewCFs}}) {
431         my $ocf = $obj->IsGlobal or next;
432         $ocf->Delete;
433         $obj->AddToObject( $_ ) for @queues;
434     }
435     $self->{NewQueues} = [];
436     $self->{NewCFs} = [];
437 }
438
439
440 sub ObjectCount {
441     my $self = shift;
442     return %{ $self->{ObjectCount} };
443 }
444
445 sub Missing {
446     my $self = shift;
447     return wantarray ? sort keys %{ $self->{Pending} }
448         : keys %{ $self->{Pending} };
449 }
450
451 sub Invalid {
452     my $self = shift;
453     return wantarray ? sort { $a->{uid} cmp $b->{uid} } @{ $self->{Invalid} }
454                      : $self->{Invalid};
455 }
456
457 sub Organization {
458     my $self = shift;
459     return $self->{Organization};
460 }
461
462 sub Progress {
463     my $self = shift;
464     return defined $self->{Progress} unless @_;
465     return $self->{Progress} = $_[0];
466 }
467
468 1;