File Coverage

blib/lib/Lingua/EN/BioLemmatizer.pm
Criterion Covered Total %
statement 110 177 62.1
branch 62 120 51.6
condition 1 11 9.0
subroutine 24 27 88.8
pod 13 13 100.0
total 210 348 60.3


line stmt bran cond sub pod time code
1             package Lingua::EN::BioLemmatizer;
2              
3             # See the include pod documentation below for description
4             # and licensing information.
5              
6             our $VERSION = 1.002;
7              
8 5     5   162531 use 5.010;
  5         19  
  5         224  
9 5     5   5503 use utf8;
  5         52  
  5         25  
10 5     5   268 use strict;
  5         16  
  5         172  
11 5     5   24 use warnings;
  5         11  
  5         187  
12 5     5   25 use warnings FATAL => "utf8";
  5         6  
  5         244  
13              
14 5     5   28 use Carp;
  5         8  
  5         519  
15              
16 5     5   29 use Scalar::Util qw(blessed reftype openhandle);
  5         10  
  5         589  
17 5     5   5232 use IO::Handle;
  5         44115  
  5         291  
18 5     5   5119 use IPC::Open2 qw(open2);
  5         28293  
  5         403  
19              
20             ########################################################################
21             ########################################################################
22             ########################################################################
23             #
24             # procedural interface
25             #
26             ########################################################################
27             ########################################################################
28             ########################################################################
29              
30 5     5   80 use base "Exporter";
  5         10  
  5         13991  
31             our @EXPORT_OK = qw(biolemma parse_response);
32              
33             sub biolemma($;$) {
34 2 50   2 1 878 croak "no arguments" if @_ == 0;
35 2 50       11 croak "too many arguments" if @_ > 2;
36 2 50       6 croak "string args only" if grep { ref() } @_;
  3         20  
37 2         22 state $server = __PACKAGE__ -> new();
38 0         0 return $server->get_biolemma(@_);
39             }
40              
41             #
42             # Input examples:
43             # "crisis NNS PennPOS"
44             # "xyzzy NONE NONE"
45             # "broken j-vvn NUPOS||break VBN PennPOS||break vvn NUPOS"
46             # "name vvz NUPOS||name VBZ PennPOS||name NNS PennPOS||name n2 NUPOS"
47             # "my po11 NUPOS||i png11 NUPOS||mine n1 NUPOS"
48             # "i pno11 NUPOS"
49             # "name NN PennPOS||name VBP PennPOS||name vvi NUPOS||name n1 NUPOS||name vvb NUPOS"
50             # "crisis NN PennPOS||crisis n1 NUPOS"
51             # "those d NUPOS"
52              
53             sub parse_response($) {
54 0 0   0 1 0 croak "no arguments" if @_ == 0;
55 0 0       0 croak "too many arguments" if @_ > 1;
56 0         0 my($string) = @_;
57 0         0 my @retlist = map { [split] } split /\Q||/, $string;
  0         0  
58 0 0       0 return wantarray() ? @retlist : \@retlist;
59             }
60              
61             ########################################################################
62             ########################################################################
63             ########################################################################
64             #
65             # object-oriented interface
66             #
67             ########################################################################
68             ########################################################################
69             ########################################################################
70              
71             # constructor for Lingua::EN::BioLemmatizer class
72             #
73             # Don't have to worry about leaking the pair of file descriptors we
74             # allocate because Perl guarantees deterministic resource management,
75             # so the IO::Handle destructor will correctly close and deallocate
76             # them when it fires eventually fires.
77             sub new {
78 8 100   8 1 9258 croak "expected args" if @_ == 0;
79              
80 7         13 my $invocant = shift();
81 7 100       24 if (ref($invocant)) {
82 2 100       10 if (blessed($invocant)) {
83 1         10 croak "constructor invoked as object method";
84             } else {
85 1         9 croak "constructor called as function with unblessed ref argument";
86             }
87             }
88              
89 5 100       35 croak "unexpected args" if @_;
90              
91 3         20 my $self = {
92             CHILD_PID => undef,
93             INTO_BIOLEMM => undef,
94             FROM_BIOLEMM => undef,
95             JAVA_PATH => __PACKAGE__ -> java_path,
96             JAVA_ARGS => [ __PACKAGE__ -> java_args ],
97             JAR_PATH => __PACKAGE__ -> jar_path,
98             JAR_ARGS => [ __PACKAGE__ -> jar_args ],
99             LEMMA_CACHE => { },
100             };
101              
102 3         13 bless($self, $invocant);
103              
104             # XXX: not sure what this is supposed to mean
105 3 50       13 if ($ENV{JAVA_HOME}) {
106 0         0 carp "warning: JAVA_HOME environment variable ignored";
107             }
108              
109 3         14 my @args = $self->command_args();
110              
111 3   0     21 my $kidpid = open2(my $pipe_from, my $pipe_into, @args)
112             // croak "can't start double-ended pipe: $!";
113              
114 0         0 for my $fh ($pipe_from, $pipe_into) {
115 0 0       0 binmode($fh, "utf8") || croak "can't binmode($fh, 'utf8'): $!";
116             }
117              
118 0         0 $self->{CHILD_PID} = $kidpid;
119 0         0 $self->{INTO_BIOLEMM} = $pipe_into;
120 0         0 $self->{FROM_BIOLEMM} = $pipe_from;
121              
122 0         0 $self->_skip_interactive_header();
123              
124 0         0 return $self;
125             }
126              
127             sub DESTROY {
128 4     4   68680 my $self = shift();
129 4 50       204 return unless ref $self;
130 4 50       67 return unless $self;
131 4         34 my $pid = $self->child_pid;
132 4 50       1065 return unless $pid;
133 0         0 close $self->into_biolemmer();
134 0         0 close $self->from_biolemmer();
135 0         0 kill TERM => $pid;
136 0         0 waitpid($pid, 0);
137             }
138              
139             # getters/setters for Lingua::EN::BioLemmatizer objects
140             #
141              
142             # only object getter allowed
143             sub child_pid {
144 5     5 1 59 my $self = shift();
145 5 100       43 croak "object method called as class method" unless ref $self;
146 4 50       29 croak "readonly method called with arguments" if @_;
147 4         29 return $self->{CHILD_PID};
148             }
149              
150             # only object getter allowed
151             sub into_biolemmer {
152 1     1 1 770 my $self = shift();
153 1 50       10 croak "object method called as class method" unless ref $self;
154 0 0       0 croak "readonly method called with arguments" if @_;
155 0         0 return $self->{INTO_BIOLEMM};
156             }
157              
158             # only object getter allowed
159             sub from_biolemmer {
160 1     1 1 810 my $self = shift();
161 1 50       12 croak "object method called as class method" unless ref $self;
162 0 0       0 croak "readonly method called with arguments" if @_;
163 0         0 return $self->{FROM_BIOLEMM};
164             }
165              
166             # only object getter allowed
167             sub lemma_cache {
168 1     1 1 949 my $self = shift();
169 1 50       14 croak "object method called as class method" unless ref $self;
170 0 0       0 croak "readonly method called with arguments" if @_;
171 0         0 return $self->{LEMMA_CACHE};
172             }
173              
174             # dual method: object getter or class getter/setter
175             sub java_path {
176 13     13 1 1713 state $_Java_Path = "java";
177 13         19 my $self = shift();
178 13 100       40 if (ref $self) {
179 3 50       12 croak "readonly method called with arguments" if @_;
180 3         31 return $self->{JAVA_PATH};
181             }
182 10 50       60 croak "expected no more than 1 argument" if @_ > 1;
183 10 100       26 $_Java_Path = $_[0] if @_;
184 10         54 return $_Java_Path;
185             }
186              
187             # dual method: object getter or class getter/setter
188             sub jar_path {
189 13   50 13 1 803 state $_Jar_Path = $ENV{BIOLEMMATIZER} || "biolemmatizer-core-1.0-jar-with-dependencies.jar";
190 13         21 my $self = shift();
191 13 100       32 if (ref $self) {
192 3 50       25 croak "readonly method called with arguments" if @_;
193 3         22 return $self->{JAR_PATH};
194             }
195 10 100       23 $_Jar_Path = $_[0] if @_;
196             ## unless (-e $_Jar_Path) { carp "cannot access $_Jar_Path: $!"; }
197 10         85 return $_Jar_Path;
198             }
199              
200             # dual method: object getter or class getter/setter
201             sub java_args {
202 15     15 1 1945 state $_Java_Args = [ "-Xmx1G", "-Dfile.encoding=utf8" ],
203             my $self = shift();
204 15 100       46 if (ref $self) {
205 3 50       11 croak "readonly method called with arguments" if @_;
206 3 50       24 return wantarray() ? @{ $self->{JAVA_ARGS} } : $self->{JAVA_ARGS};
  3         19  
207             }
208              
209 12 100       45 if (@_ == 1) {
    50          
210 2         3 my $arg = shift();
211 2 50       6 if (ref($arg)) {
212 2 50       8 croak "unexpected non-arrayref arg" unless ref($arg) eq "ARRAY";
213 2         4 $_Java_Args = $arg;
214             }
215             else {
216 0         0 $_Java_Args = [ $arg ];
217             }
218             }
219             elsif (@_ > 1) {
220 0 0       0 croak "unexpected ref arg" if grep { ref() } @_;
  0         0  
221 0         0 $_Java_Args = [ @_ ];
222             }
223             else {
224             # FALLTHROUGH
225             }
226              
227 12 100       34 return wantarray() ? @{ $_Java_Args } : $_Java_Args;
  6         30  
228             }
229              
230             # dual method: object getter or class getter/setter
231             sub jar_args {
232 15     15 1 2304 state $_Jar_Args = [ "-t" ],
233             my $self = shift();
234 15 100       47 if (ref $self) {
235 3 50       11 croak "readonly method called with arguments" if @_;
236 3 50       10 return wantarray() ? @{ $self->{JAR_ARGS} } : $self->{JAR_ARGS};
  3         14  
237             }
238              
239 12 100       43 if (@_ == 1) {
    50          
240 2         3 my $arg = shift();
241 2 50       5 if (ref($arg)) {
242 2 50       8 croak "unexpected non-arrayref arg" unless ref($arg) eq "ARRAY";
243 2         4 $_Jar_Args = $arg;
244             }
245             else {
246 0         0 $_Jar_Args = [ $arg ];
247             }
248             }
249             elsif (@_ > 1) {
250 0 0       0 croak "unexpected ref arg" if grep { ref() } @_;
  0         0  
251 0         0 $_Jar_Args = [ @_ ];
252             }
253             else {
254             # FALLTHROUGH
255             }
256              
257 12 100       32 return wantarray() ? @{ $_Jar_Args } : $_Jar_Args;
  6         60  
258             }
259              
260             ########################################################################
261              
262             #################
263             # other methods
264             #################
265              
266             # dual class/object method
267             sub command_args {
268 6     6 1 1731 my $invocant = shift();
269 6 100       47 croak "unexpected args" if @_;
270              
271 5         22 my @args = (
272             $invocant->java_path,
273             $invocant->java_args,
274             "-jar",
275             $invocant->jar_path,
276             $invocant->jar_args,
277             );
278              
279 5 100       33 return wantarray() ? (@args) : "@args";
280             }
281              
282             # object method only, not class method
283             sub get_biolemma {
284 1     1 1 479 my $self = shift();
285 1 50       11 croak "object method called as class method" unless ref $self;
286              
287 0         0 my($orig, $pos);
288              
289 0         0 given(scalar(@_)) {
290 0         0 when (2) { ($orig, $pos) = @_ }
  0         0  
291 0         0 when (1) { ($orig) = @_ }
  0         0  
292 0         0 when (0) { croak "expected 1 or 2 args" }
  0         0  
293 0         0 default { croak "too many args" }
  0         0  
294             }
295              
296 0         0 my $cache_ref = $self->lemma_cache;
297              
298 0         0 my $string = $orig;
299 0 0       0 $string .= " $pos" if $pos;
300              
301             # This funny "||=" move (sometimes called the "orkish maneuver"
302             # == "OR-cache maneuver") loads cache only if that slot previously
303             # false, then returns whatever is there.
304             #
305 0   0     0 return $cache_ref->{$string} ||= $self->_handle_request($string);
306             }
307              
308             # private object method only, not class method
309             sub _handle_request {
310 2 100   2   1686 croak "don't call private methods" unless caller eq __PACKAGE__;
311 1         41 my $self = shift();
312 1 50       14 croak "object method called as class method" unless ref $self;
313 0 0         croak "expected just one arg" unless @_ == 1;
314              
315 0           my($request) = @_;
316              
317 0           $request =~ s/\R+\z//; # remove any trailing linebreaks
318             # lest a blank line kill server
319              
320             {
321 0     0     local $SIG{PIPE} = sub { croak "biolemmatizer pipe broke" };
  0            
  0            
322 0           print { $self->into_biolemmer } $request, "\n";
  0            
323             }
324              
325 0   0       my $response = $self->from_biolemmer->getline()
326             // croak "no return string from biolemmatizer";
327              
328 0           $response =~ s/\R+\z//;
329 0           return $response;
330             }
331              
332             # private object method to skip the noisy preamble strings
333             # that it spits out when the process is first started up.
334             # The exact header is the five lines following, without the ##:
335              
336             ## =========================================================
337             ## =========================================================
338             ## =========================================================
339             ## Running BioLemmatizer....
340             ## Running BioLemmatizer in interactive mode. Please type a word to be lemmatized with an optional part-of-speech, e.g. "run" or "run NN"
341              
342             sub _skip_interactive_header {
343 0 0   0     croak "don't call private methods" unless caller eq __PACKAGE__;
344 0           my $self = shift();
345 0 0         croak "object method called as class method" unless ref $self;
346 0 0         croak "expected no args" unless @_ == 0;
347              
348 0           my $fh = $self->from_biolemmer();
349              
350             # NB: these are blockings reads.
351              
352             # three lines of equal signs
353 0           for (1..3) {
354 0 0         croak "unexpected preamble: no ===" unless <$fh> =~ /^===/;
355             }
356             # then two lines of starting with this
357 0           for (1..2) {
358 0 0         croak "unexpected preamble: no greeting" unless <$fh> =~ /^Running BioLemmatizer/;
359             }
360             }
361              
362              
363             # last "1;" is so that "use" and "require"
364             # consider the module properly intialized
365             #
366             1; # don't delete this!
367              
368             __END__