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