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