1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Migrate::Importer;
60 my $self = bless {}, $class;
73 ExcludeOrganization => undef,
77 # Should we attempt to preserve record IDs as they are created?
78 $self->{OriginalId} = $args{OriginalId};
80 $self->{ExcludeOrganization} = $args{ExcludeOrganization};
82 $self->{Progress} = $args{Progress};
84 $self->{HandleError} = sub { 0 };
85 $self->{HandleError} = $args{HandleError}
86 if $args{HandleError} and ref $args{HandleError} eq 'CODE';
88 if ($args{DumpObjects}) {
90 $self->{DumpObjects} = { map { $_ => 1 } @{$args{DumpObjects}} };
93 # Objects we've created
96 # Columns we need to update when an object is later created
97 $self->{Pending} = {};
99 # Objects missing from the source database before serialization
100 $self->{Invalid} = [];
103 $self->{ObjectCount} = {};
105 # To know what global CFs need to be unglobal'd and applied to what
106 $self->{NewQueues} = [];
107 $self->{NewCFs} = [];
112 return $self->{Metadata};
119 return if $self->{Metadata};
120 $self->{Metadata} = $data;
122 die "Incompatible format version: ".$data->{Format}
123 if $data->{Format} ne "0.8";
125 $self->{Organization} = $data->{Organization};
126 $self->{Clone} = $data->{Clone};
127 $self->{Incremental} = $data->{Incremental};
128 $self->{Files} = $data->{Files} if $data->{Final};
134 die "Stream initialized after objects have been recieved!"
135 if keys %{ $self->{UIDs} };
137 die "Cloning does not support importing the Original Id separately\n"
138 if $self->{OriginalId} and $self->{Clone};
140 die "RT already contains data; overwriting will not work\n"
141 if ($self->{Clone} and not $self->{Incremental})
142 and RT->SystemUser->Id;
144 # Basic facts of life, as a safety net
145 $self->Resolve( RT->System->UID => ref RT->System, RT->System->Id );
146 $self->SkipTransactions( RT->System->UID );
148 if ($self->{OriginalId}) {
149 # Where to shove the original ticket ID
150 my $cf = RT::CustomField->new( RT->SystemUser );
151 $cf->LoadByName( Name => $self->{OriginalId}, LookupType => RT::Ticket->CustomFieldLookupType, ObjectId => 0 );
153 warn "Failed to find global CF named $self->{OriginalId} -- creating one";
156 Name => $self->{OriginalId},
157 Type => 'FreeformSingle',
165 my ($uid, $class, $id) = @_;
166 $self->{UIDs}{$uid} = [ $class, $id ];
167 return unless $self->{Pending}{$uid};
169 for my $ref (@{$self->{Pending}{$uid}}) {
170 my ($pclass, $pid) = @{ $self->Lookup( $ref->{uid} ) };
171 my $obj = $pclass->new( RT->SystemUser );
172 $obj->LoadByCols( Id => $pid );
174 Field => $ref->{column},
176 ) if defined $ref->{column};
178 Field => $ref->{classcolumn},
180 ) if defined $ref->{classcolumn};
182 Field => $ref->{uri},
183 Value => $self->LookupObj($uid)->URI,
184 ) if defined $ref->{uri};
185 if (my $method = $ref->{method}) {
186 $obj->$method($self, $ref, $class, $id);
189 delete $self->{Pending}{$uid};
195 unless (defined $uid) {
196 carp "Tried to lookup an undefined UID";
199 return $self->{UIDs}{$uid};
205 my $ref = $self->Lookup( $uid );
207 my ($class, $id) = @{ $ref };
209 my $obj = $class->new( RT->SystemUser );
220 classcolumn => undef,
224 my $uid = delete $args{for};
227 push @{$self->{Pending}{$uid}}, \%args;
229 push @{$self->{Invalid}}, \%args;
233 sub SkipTransactions {
236 return if $self->{Clone};
237 $self->{SkipTransactions}{$uid} = 1;
240 sub ShouldSkipTransaction {
243 return exists $self->{SkipTransactions}{$uid};
248 my ($obj, $data) = @_;
249 for my $col (keys %{$data}) {
250 next if defined $obj->__Value($col) and length $obj->__Value($col);
251 next unless defined $data->{$col} and length $data->{$col};
253 if (ref $data->{$col}) {
254 my $uid = ${ $data->{$col} };
255 my $ref = $self->Lookup( $uid );
257 $data->{$col} = $ref->[1];
267 $obj->__Set( Field => $col, Value => $data->{$col} );
273 my ($column, $class, $uid, $data) = @_;
275 my $obj = $class->new( RT->SystemUser );
276 $obj->Load( $data->{$column} );
277 return unless $obj->Id;
279 $self->SkipTransactions( $uid );
281 $self->Resolve( $uid => $class => $obj->Id );
287 my ($column, $class, $uid, $data) = @_;
289 my $obj = $self->SkipBy(@_);
291 $self->MergeValues( $obj, $data );
298 return $string if $self->{Clone};
299 return $string if not defined $self->{Organization};
300 return $string if $self->{ExcludeOrganization};
301 return $string if $self->{Organization} eq $RT::Organization;
302 return $self->{Organization}.": $string";
307 my ($class, $uid, $data) = @_;
309 # Use a simpler pre-inflation if we're cloning
310 if ($self->{Clone}) {
311 $class->RT::Record::PreInflate( $self, $uid, $data );
313 # Non-cloning always wants to make its own id
315 return unless $class->PreInflate( $self, $uid, $data );
318 my $obj = $class->new( RT->SystemUser );
319 my ($id, $msg) = eval {
320 # catch and rethrow on the outside so we can provide more info
322 $obj->DBIx::SearchBuilder::Record::Create(
327 $msg ||= ''; # avoid undef
328 my $err = "Failed to create $uid: $msg $@\n" . Data::Dumper::Dumper($data) . "\n";
329 if (not $self->{HandleError}->($self, $err)) {
336 $self->{ObjectCount}{$class}++;
337 $self->Resolve( $uid => $class, $id );
339 # Load it back to get real values into the columns
340 $obj = $class->new( RT->SystemUser );
342 $obj->PostInflate( $self, $uid );
351 no warnings 'redefine';
352 local *RT::Ticket::Load = sub {
355 $self->LoadById( $id );
359 my $loaded = Storable::fd_retrieve($fh);
361 # Metadata is stored at the start of the stream as a hashref
362 if (ref $loaded eq "HASH") {
363 $self->LoadMetadata( $loaded );
368 my ($class, $uid, $data) = @{$loaded};
370 if ($self->{Incremental}) {
371 my $obj = $class->new( RT->SystemUser );
372 $obj->Load( $data->{id} );
374 # undef $uid means "delete it"
376 $self->{ObjectCount}{$class}++;
377 } elsif ( $obj->Id ) {
378 # If it exists, update it
379 $class->RT::Record::PreInflate( $self, $uid, $data );
380 $obj->__Set( Field => $_, Value => $data->{$_} )
382 $self->{ObjectCount}{$class}++;
385 $obj = $self->Create( $class, $uid, $data );
387 $self->{Progress}->($obj) if $obj and $self->{Progress};
389 } elsif ($self->{Clone}) {
390 my $obj = $self->Create( $class, $uid, $data );
391 $self->{Progress}->($obj) if $obj and $self->{Progress};
395 # If it's a queue, store its ID away, as we'll need to know
396 # it to split global CFs into non-global across those
397 # fields. We do this before inflating, so that queues which
398 # got merged still get the CFs applied
399 push @{$self->{NewQueues}}, $uid
400 if $class eq "RT::Queue";
402 my $origid = $data->{id};
403 my $obj = $self->Create( $class, $uid, $data );
406 # If it's a ticket, we might need to create a
407 # TicketCustomField for the previous ID
408 if ($class eq "RT::Ticket" and $self->{OriginalId}) {
409 my $value = $self->{ExcludeOrganization}
411 : $self->Organization . ":$origid";
413 my ($id, $msg) = $obj->AddCustomFieldValue(
414 Field => $self->{OriginalId},
416 RecordTransaction => 0,
418 warn "Failed to add custom field to $uid: $msg"
422 # If it's a CF, we don't know yet if it's global (the OCF
423 # hasn't been created yet) to store away the CF for later
425 push @{$self->{NewCFs}}, $uid
426 if $class eq "RT::CustomField"
427 and $obj->LookupType =~ /^RT::Queue/;
429 $self->{Progress}->($obj) if $self->{Progress};
435 $self->{Progress}->(undef, 'force') if $self->{Progress};
437 return if $self->{Clone};
439 # Take global CFs which we made and make them un-global
440 my @queues = grep {$_} map {$self->LookupObj( $_ )} @{$self->{NewQueues}};
441 for my $obj (map {$self->LookupObj( $_ )} @{$self->{NewCFs}}) {
442 my $ocf = $obj->IsGlobal or next;
444 $obj->AddToObject( $_ ) for @queues;
446 $self->{NewQueues} = [];
447 $self->{NewCFs} = [];
453 return %{ $self->{ObjectCount} };
458 return wantarray ? sort keys %{ $self->{Pending} }
459 : keys %{ $self->{Pending} };
464 return wantarray ? sort { $a->{uid} cmp $b->{uid} } @{ $self->{Invalid} }
470 return $self->{Organization};
475 return defined $self->{Progress} unless @_;
476 return $self->{Progress} = $_[0];