be96f4660f1e5894f00771c42b7fa08f4fca5168
[migration-tools.git] / Equinox-Migration / lib / Equinox / Migration / MapDrivenMARCXMLProc.pm
1 package Equinox::Migration::MapDrivenMARCXMLProc;
2
3 use warnings;
4 use strict;
5
6 use XML::Twig;
7 use Equinox::Migration::SubfieldMapper 1.003;
8
9
10 =head1 NAME
11
12 Equinox::Migration::MapDrivenMARCXMLProc
13
14 =head1 VERSION
15
16 Version 1.000
17
18 =cut
19
20 our $VERSION = '1.000';
21
22
23 =head1 SYNOPSIS
24
25 Foo
26
27     use Equinox::Migration::MapDrivenMARCXMLProc;
28
29
30 =head1 METHODS
31
32
33 =head2 new
34
35 Takes two required arguments: C<mapfile> (which will be passed along
36 to L<Equinox::Migration::SubfieldMapper> as the basis for its map),
37 and C<marcfile> (the MARC data to be processed).
38
39     my $m = Equinox::Migration::MapDrivenMARCXMLProc->new( mapfile  => FILE,
40                                                            marcfile => FILE );
41
42 =cut
43
44 sub new {
45     my ($class, %args) = @_;
46
47     my $self = bless { mods => { multi    => {},
48                                  required => {},
49                                },
50                        data => { recs => undef, # X::T record objects
51                                  rptr => 0,     # next record pointer
52                                  crec => undef, # parsed record storage
53                                  tmap => undef, # tag_id-to-tag array map
54                                },
55                      }, $class;
56
57     # initialize map and taglist
58     die "Argument 'mapfile' must be specified\n" unless (defined $args{mapfile});
59     my @mods = keys %{$self->{mods}};
60     $self->{map} = Equinox::Migration::SubfieldMapper->new( file => $args{mapfile},
61                                                             mods => \@mods );
62     $self->{data}{tags} = $self->{map}->tags;
63
64     # initialize twig
65     die "Argument 'marcfile' must be specified\n" unless (defined $args{marcfile});
66     if (-r $args{marcfile}) {
67         $self->{twig} = XML::Twig->new;
68         $self->{twig}->parsefile($args{marcfile});
69         my @records = $self->{twig}->root->children;
70         $self->{data}{recs} = \@records;
71     } else {
72         die "Can't open marc file: $!\n";
73     }
74
75     return $self;
76 }
77
78
79 =head2 parse_record
80
81 Extracts data from the next record, per the mapping file. Returns a
82 normalized datastructure (see L</format_record> for details) on
83 success; returns 0 otherwise.
84
85     while (my $rec = $m->parse_record) {
86       # handle extracted record data
87     }
88
89 =cut
90
91 sub parse_record {
92     my ($self) = @_;
93
94     # get the next record and wipe current parsed record
95     return 0 unless defined $self->{data}{recs}[ $self->{data}{rptr} ];
96     my $record = $self->{data}{recs}[ $self->{data}{rptr} ];
97     $self->{data}{crec} = { egid => undef, tags => undef };
98     $self->{data}{tmap} = {};
99
100     my @fields = $record->children;
101     for my $f (@fields)
102       { $self->process_field($f) }
103
104     # cleanup memory and increment pointer
105     $record->purge;
106     $self->{data}{rptr}++;
107
108     # check for required fields
109     $self->check_required;
110
111     return $self->{data}{crec};
112 }
113
114 sub process_field {
115     my ($self, $field) = @_;
116     my $map = $self->{map};
117     my $tag = $field->{'att'}->{'tag'};
118     my $crec = $self->{data}{crec};
119     my $tmap = $self->{data}{tmap};
120
121     # leader
122     unless (defined $tag) {
123         #FIXME
124         return;
125     }
126
127     # datafields
128     if ($tag == 903) {
129         my $sub = $field->first_child('subfield');
130         $crec->{egid} = $sub->text;
131         return;
132     }
133     if ($map->has($tag)) {
134         push @{$crec->{tags}}, { tag => $tag, uni => undef, multi => undef };
135         push @{$tmap->{$tag}}, (@{$crec->{tags}} - 1);
136         my @subs = $field->children('subfield');
137         for my $sub (@subs)
138           { $self->process_subs($tag, $sub) }
139         # check map to ensure all declared subs have a value
140         my $mods = $map->mods($field);
141         for my $mappedsub ( @{ $map->subfields($tag) } ) {
142             next if $mods->{multi};
143             $crec->{tags}[-1]{uni}{$mappedsub} = ''
144               unless defined $crec->{tags}[-1]{uni}{$mappedsub};
145         }
146     }
147 }
148
149 sub process_subs {
150     my ($self, $tag, $sub) = @_;
151     my $map  = $self->{map};
152     my $code = $sub->{'att'}->{'code'};
153
154     # handle unmapped tag/subs
155     return unless ($map->has($tag, $code));
156
157     # fetch our datafield struct and fieldname
158     my $dataf = $self->{data}{crec}{tags}[-1];
159     my $field = $map->field($tag, $code);
160
161     # test filters
162     for my $filter ( @{$map->filters($field)} ) {
163         return if ($sub->text =~ /$filter/i);
164     }
165     # handle multi modifier
166     if (my $mods = $map->mods($field)) {
167         if ($mods->{multi}) {
168             my $name = $tag . $code;
169             push @{$dataf->{multi}{$name}}, $sub->text;
170             return;
171         }
172     }
173
174     # if this were a multi field, it would be handled already. make sure its a singleton
175     die "Multiple occurances of a non-multi field: $tag$code at rec ",
176       ($self->{data}{rptr} + 1),"\n" if (defined $dataf->{uni}{$code});
177
178     # everything seems okay
179     $dataf->{uni}{$code} = $sub->text;
180 }
181
182
183 sub check_required {
184     my ($self) = @_;
185     my $mods = $self->{map}->mods;
186     my $crec = $self->{data}{crec};
187
188     for my $tag_id (keys %{$mods->{required}}) {
189         for my $code (@{$mods->{required}{$tag_id}}) {
190             my $found = 0;
191
192             for my $tag (@{$crec->{tags}}) {
193                 $found = 1 if ($tag->{multi}{($tag_id . $code)});
194                 $found = 1 if ($tag->{uni}{$code});
195             }
196
197             die "Required mapping $tag_id$code not found in rec ",$self->{data}{rptr},"\n"
198               unless ($found);
199         }
200     }
201
202 }
203
204 =head1 MODIFIERS
205
206 MapDrivenMARCXMLProc implements the following modifiers, and passes
207 them to L<Equinox::Migration::SubfieldMapper>, meaning that specifying
208 any other modifiers in a MDMP map file will cause a fatal error when
209 it is processed.
210
211 =head2 multi
212
213 If a mapping is declared to be C<multi>, then MDMP expects to see more
214 than one instance of that subfield per datafield, and the data is
215 handled accordingly (see L</PARSED RECORDS> below).
216
217 Occurring zero or one time is legal for a C<multi> mapping.
218
219 A mapping which is not flagged as C<multi>, but which occurs more than
220 once per datafield will cause a fatal error.
221
222 =head2 required
223
224 By default, if a mapping does not occur in a datafield, processing
225 continues normally. if a mapping has the C<required> modifier,
226 however, it must appear, or a fatal error will occur.
227
228 =head1 PARSED RECORDS
229
230 Given:
231
232     my $m = Equinox::Migration::MapDrivenMARCXMLProc->new(ARGUMENTS);
233     $rec = $m->parse_record;
234
235 Then C<$rec> will look like:
236
237     {
238       egid => evergreen_record_id,
239       tags => [
240                 {
241                   tag   => tag_id,
242                   multi => { (tag_id . sub_code) => [ val1, val2, ... ] },
243                   uni   => { code => value, code2 => value2, ... },
244                 },
245                 ...
246               ]
247     }
248
249 That is, there is an C<egid> key which points to the Evergreen ID of
250 that record, and a C<tags> key which points to an arrayref.
251
252 =head3 C<tags>
253
254 A reference to a list of anonymous hashes, one for each instance of
255 each tag which occurs in the map.
256
257 Each tag hash holds its own id (e.g. C<998>), and two references to
258 two more hashrefs, C<multi> and C<uni>.
259
260 The C<multi> hash holds the extracted data for tag/sub mappings which
261 have the C<multiple> modifier on them. The keys in C<multi> are
262 composed of the tag id and subfield code, catenated
263 (e.g. C<901c>). The values are arrayrefs containing the content of all
264 instances of that subfield in that instance of that tag. If no tags
265 are defined as C<multi>, it will be C<undef>.
266
267 The C<uni> hash holds data for tag/sub mappings which occur only once
268 per instance of a tag (but may occur multiple times in a record due to
269 there being multiple instances of that tag in a record). Keys are
270 subfield codes and values are subfield content.
271
272 All C<uni> subfields occuring in the map are guaranteed to be
273 defined. Sufields which are mapped but do not occur in a particular
274 datafield will be given a value of '' (the null string) in the current
275 record struct. Oppose subfields which are not mapped, which will be
276 C<undef>.
277
278
279 =head1 AUTHOR
280
281 Shawn Boyette, C<< <sboyette at esilibrary.com> >>
282
283 =head1 BUGS
284
285 Please report any bugs or feature requests to the above email address.
286
287 =head1 SUPPORT
288
289 You can find documentation for this module with the perldoc command.
290
291     perldoc Equinox::Migration::MapDrivenMARCXMLProc
292
293
294 =head1 COPYRIGHT & LICENSE
295
296 Copyright 2009 Equinox, all rights reserved.
297
298 This program is free software; you can redistribute it and/or modify it
299 under the same terms as Perl itself.
300
301
302 =cut
303
304 1; # End of Equinox::Migration::MapDrivenMARCXMLProc