359112263f9bcc51f5b2d2d19e0917f9a35db094
[migration-tools.git] / Equinox-Migration / lib / Equinox / Migration / SubfieldMapper.pm
1 package Equinox::Migration::SubfieldMapper;
2
3 use warnings;
4 use strict;
5
6 =head1 NAME
7
8 Equinox::Migration::SubfieldMapper - Generate named-field to MARC tag map from file
9
10 =head1 VERSION
11
12 Version 1.005
13
14 =cut
15
16 our $VERSION = '1.005';
17
18
19 =head1 SYNOPSIS
20
21 Using a file as input, E::M::SM generates a mapping of MARC subfields
22 to arbitrary field names, and provides several access mechanisms to
23 that set.
24
25     use Equinox::Migration::SubfieldMapper;
26     ...
27
28
29 =head1 METHODS
30
31 =head2 new
32
33 Takes one optional argument, C<file>. If this is speficied, the tag
34 list will be populated as per that file on instantiation.
35
36 Returns a E::M::SM object.
37
38 =cut
39
40 sub new {
41     my ($class, %args) = @_;
42
43     my $self = bless { conf   => { mods => undef },
44                        fields => {},
45                        tags   => {} }, $class;
46
47     if ($args{mods}) {
48         die "Argument 'mods' is wrong type\n"
49           unless (ref $args{mods} eq "ARRAY");
50         for my $mod ( @{$args{mods}} )
51           { $self->{conf}{mods}{$mod} = 1 }
52     }
53
54     if ($args{file}) {
55         if (-r $args{file}) {
56             $self->{conf}{file} = $args{file};
57             $self->generate;
58         } else {
59             die "Can't open file: $!\n";
60         }
61     }
62
63     return $self;
64 }
65
66 =head2 has
67
68 Ask it whether your mapping has various things, and it'll let you know.
69
70     $sm->has('fieldname')      # is this fieldname mapped?
71     $sm->has(901)              # are there any mappings for this tag?
72     $sm->has(650,'c')          # is this tag/subfield combo mapped?
73     $sm->has('name', 245, 'a') # is this name mapped to 245$a?
74
75 Returns 1 if true, 0 if false.
76
77 FIXME: use named params instead of positional
78
79 =cut
80
81 sub has {
82     my ($self, @chunks) = @_;
83     return undef unless (defined $chunks[0]);
84
85     if ($chunks[0] =~ /^\d/) {
86         if (defined $chunks[1]) {
87             return 1 if ( defined $self->{tags}{$chunks[0]}{$chunks[1]} );
88             return 0;
89         } else {
90             return 1 if ( defined $self->{tags}{$chunks[0]} );
91             return 0;
92         }
93     } else {
94         if (defined $chunks[2]) {
95             return 1 if ( $self->{fields}{$chunks[0]}{tag} eq $chunks[1] and
96                           $self->{fields}{$chunks[0]}{sub} eq $chunks[2] );
97             return 0;
98         } elsif (defined $chunks[1]) {
99             return 1 if ( $self->{fields}{$chunks[0]}{tag} eq $chunks[1] );
100             return 0;
101         } else {
102             return 1 if ( defined $self->{fields}{$chunks[0]} );
103             return 0;
104         }
105     }
106 }
107
108 =head2 tags
109
110 Returns an arrayref containing the tags defined in the map.
111
112     my $tags = $sfm->tags;
113     for my tag ( @{$tags} ) {
114         my $subs = $sfm->subfields($tag);
115         ...
116     }
117
118 =cut
119
120 sub tags {
121     my ($self) = @_;
122     return [ keys %{$self->{tags}} ];
123 }
124
125 =head2 subfields
126
127 Given a tag, return an arrayref of the subfields mapped with that tag.
128
129     my $tags = $sfm->tags;
130     for my tag ( @{$tags} ) {
131         my $subs = $sfm->subfields($tag);
132         ...
133     }
134
135 Returns C<undef> if C<tag> is not mapped.
136
137 =cut
138
139 sub subfields {
140     my ($self, $tag) = @_;
141     return undef unless $self->has($tag);
142     return [ keys %{$self->{tags}{$tag}} ];
143 }
144
145
146 =head2 field
147
148 Given a tag and subfield code,
149
150     my $fname = $sm->field(945, 'p')
151
152 return the name mapped to them. Returns C<undef> if no mapping exists.
153
154 =cut
155
156 sub field {
157     my ($self, $tag, $sub) = @_;
158     return undef unless (defined $tag and defined $sub);
159     return undef unless $self->has($tag, $sub);
160     return $self->{tags}{$tag}{$sub};
161 }
162
163 =head2 mods
164
165 With no argument, returns a hashref containing all modifiers for the entire map:
166
167     {
168       modifier => {
169                     tag => [ list_of subs ],
170                     ...
171                   },
172       ...
173     }
174
175 Given a fieldname, returns a hashref of the modifiers set on that mapping.
176
177     $self->mods('fieldname')
178
179 Returns undef is nothing is defined.
180
181 =cut
182
183 sub mods {
184     my ($self, $field) = @_;
185     return $self->{allmods} unless defined $field;
186     return undef unless $self->has($field);
187     return undef unless (%{ $self->{fields}{$field}{mods} });
188     return $self->{fields}{$field}{mods};
189 }
190
191 =head2 filters
192
193 Returns the content filters set on a mapping
194
195     $self->filters('fieldname')
196
197 If there are no filters, C<undef> will be returned. Else a listref
198 will be returned.
199
200 =cut
201
202 sub filters {
203     my ($self, $field) = @_;
204     return undef unless $self->has($field);
205     return undef unless ($self->{fields}{$field}{filt});
206     return $self->{fields}{$field}{filt};
207 }
208
209 =head2 sep
210
211 Returns the separator string set on a mapping.  Used only
212 if concatenating.
213
214 =cut
215
216 sub sep {
217     my ($self, $field) = @_;
218     return undef unless $self->has($field);
219     return $self->{fields}{$field}{sep};
220 }
221
222 =head1 MAP CONSTRUCTION METHODS
223
224 These methods are not generally accessed from user code.
225
226 =head2 generate
227
228 Generate initial mapping from file.
229
230 =cut
231
232 sub generate {
233     my ($self, $file) = @_;
234
235     open TAGFILE, '<', $self->{conf}{file};
236     while (<TAGFILE>) {
237         next if m/^#/;
238         next if m/^\s*\n$/;
239
240         chomp;
241         my @tokens = split /\s+/;
242
243         my $map = { mods => [], filt => [], sep => ' ' };
244         $map->{field} = shift @tokens;
245         $map->{tag}   = shift @tokens;
246         while (defined (my $tok = shift @tokens)) {
247             last if ($tok =~ m/^#/);
248             if ($tok =~ m/^[a-zA-Z]:'/ and $tok !~ /^'$/) {
249                 $tok .= ' ' . shift @tokens
250                   until ($tokens[0] =~ m/'$/);
251                 $tok .= ' ' . shift @tokens;
252                 $tok =~ s/'//;
253                 $tok =~ s/'$//;
254             }
255             if ($tok =~ m/^m:/)
256               { push @{$map->{mods}}, $tok }
257             elsif ($tok =~ m/^f:/)
258               { push @{$map->{filt}}, $tok }
259             elsif ($tok =~ m/^[a-zA-Z0-9]$/)
260               { $map->{sub} = $tok }
261             elsif ($tok =~ /^c:(.*)$/)
262               { $map->{sep} = $1 }
263             else
264               { die "Unknown chunk '$tok' at line $.\n" }
265         }
266         $self->add($map);
267     }
268 }
269
270 =head2 add
271
272 Add new item to mapping. Not usually called directly from user code.
273
274     $sm->add( $map );
275
276 Where C<$map> is a hashref that, at a minimum, looks like
277
278     { field => "value", tag => NNN, sub => X }
279
280 and may also have the key/value pairs
281
282     mods => [ ITEMS ]
283     filt => [ ITEMS ]
284
285 =cut
286
287 sub add {
288     my ($self, $map) = @_;
289
290     # trim the mods and filters
291     my $mods = {};
292     my $filt = []; my %filt = ();
293     for my $m (@{$map->{mods}}) {
294         die "Modifier collision '$m' at line $." if $mods->{$m};
295         $m =~ s/^m://;
296         $mods->{$m} = 1;
297         push @{$self->{allmods}{$m}{ $map->{tag} }}, $map->{sub};
298     }
299     for my $f (@{$map->{filt}}) {
300         die "Filter collision '$f' at line $." if $filt{$f};
301         $f =~ s/^f://;
302         push @{$filt}, $f; $filt{$f} = 1;
303     }
304     $map->{mods} = $mods;
305     $map->{filt} = $filt;
306
307     # check bits for validity
308     $self->validate($map);
309
310     # add data to the fields hash
311     $self->{fields}{ $map->{field} } = { tag => $map->{tag},
312                                          sub => $map->{sub},
313                                          mods => $map->{mods},
314                                          filt => $map->{filt},
315                                          sep => $map->{sep},
316                                        };
317     # and to the tags hash
318     $self->{tags}{ $map->{tag} }{ $map->{sub} } = $map->{field};
319 }
320
321 =head2 validate
322
323 Passed a reference to the hash given to C<add>, validate scans its
324 contents for common errors and dies if there is an issue.
325
326     * field, tag, and sub are required
327     * fieldnames must start with a letter
328     * fieldnames must be unique
329     * tag must be between 0 and 999
330     * subfield code must be a single alphanumeric character
331     * tag+subfield can only be mapped once
332     * if a list of allowable mod values was given in the call to
333       C<new>, any modifiers must be on that list
334
335 =cut
336
337 sub validate {
338     my ($self, $map) = @_;
339
340     $.= 1 unless defined $.;
341
342     die "Required field missing (line $.)\n"
343       unless (defined $map->{field} and defined $map->{tag} and defined $map->{sub});
344
345     die "Fieldnames must start with letter (line $.)\n"
346      unless ($map->{field} =~ /^[a-zA-z]/);
347
348     die "Invalid tag (line $.)\n"
349       if ($map->{tag} =~ /[^\d\-]/ or $map->{tag} < 0 or $map->{tag} > 999);
350
351     die "Invalid subfield code (line $.)\n"
352       if (length $map->{sub} != 1 or $map->{sub} =~ /[^a-zA-Z0-9]/);
353
354     # test mod names if we have a set to check against
355     if (defined $self->{conf}{mods}) {
356         for my $mod ( keys %{$map->{mods}} ) {
357             die "Modifier '$mod' not allowed\n"
358               unless $self->{conf}{mods}{$mod};
359         }
360     }
361
362     die "Fieldnames must be unique (line $.)\n"
363       if (defined $self->{fields}{$map->{field}});
364
365     die "Subfields cannot be mapped twice (line $.)\n"
366       if (defined $self->{tags}{$map->{tag}}{$map->{sub}});
367
368 }
369
370
371 =head1 AUTHOR
372
373 Shawn Boyette, C<< <sboyette at esilibrary.com> >>
374
375 =head1 BUGS
376
377 Please report any bugs or feature requests to the above email address.
378
379 =head1 SUPPORT
380
381 You can find documentation for this module with the perldoc command.
382
383     perldoc Equinox::Migration::SubfieldMapper
384
385
386 =head1 COPYRIGHT & LICENSE
387
388 Copyright 2009 Equinox, all rights reserved.
389
390 This program is free software; you can redistribute it and/or modify it
391 under the same terms as Perl itself.
392
393
394 =cut
395
396 1; # End of Equinox::Migration::SimpleTagList