* Add --invalidate-state option to specifically invalidate a state
[function2gene.git] / bin / function2gene
1 #! /usr/bin/perl
2 # function2gene, is part of the function2gene suite and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
6
7
8 use threads;
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 use Storable;
16
17 =head1 NAME
18
19   function2gene - Call out to each of the search modules to search for
20   each of the terms
21
22 =head1 SYNOPSIS
23
24  function2gene --keywords keywords.txt --results gene_search_results
25
26  Options:
27   --keywords newline delineated list of keywords to search for
28   --results directory to store results in
29   --database databases to search
30   --restart-at mode to start searching at
31   --invalidate-state state to invalidate
32   --debug, -d debugging level (Default 0)
33   --help, -h display this help
34   --man, -m display manual
35
36 =head1 OPTIONS
37
38 =over
39
40 =item B<--keywords>
41
42 A file which contains a newline delinated list of keywords to search
43 for. Can be specified multiple times. Lines starting with # or ; are
44 ignored.
45
46 =item B<--results>
47
48 Directory in which to store results; also stores the current state of
49 the system
50
51 =item B<--database>
52
53 Databases to search, can be specified multiple times. [Defaults to
54 NCBI, GeneCards and Harvester, the only currently supported
55 databases.]
56
57 =item B<--restart-at>
58
59 If you need to restart the process at a particular state (which has
60 already been completed) specify this option. Valid values are get,
61 parse, or combine.
62
63 =item B<--invalidate-state>
64
65 This is a more powerful version of --restart-at, which can
66 specifically invalidate a certain method,database,keyword combination.
67
68 For example, you can request that the keyword foo be retreived again
69 from ncbi using --invalidate-state 'get,ncbi,foo'
70
71 =item B<--debug, -d>
72
73 Debug verbosity. (Default 0)
74
75 =item B<--help, -h>
76
77 Display brief useage information.
78
79 =item B<--man, -m>
80
81 Display this manual.
82
83 =back
84
85 =head1 EXAMPLES
86
87    # Search all databases for transferrin
88    echo 'transferrin' > keywords.txt
89    function2gene --keywords keywords.txt --results keyword_results
90
91    # reparse the results
92    function2gene --keywords keywords.txt --results keyword_results \
93        --restart-at parse
94
95 =cut
96
97
98 use vars qw($DEBUG);
99 use Cwd qw(abs_path);
100 use IO::File;
101 use Storable qw(thaw freeze);
102 use File::Basename qw(basename dirname);
103 use Thread::Queue;
104
105 my %options = (databases       => [],
106                keywords        => [],
107                debug           => 0,
108                help            => 0,
109                man             => 0,
110                results         => '',
111                invalidate_state => [],
112                );
113
114 GetOptions(\%options,'keywords=s@','databases=s@',
115            'restart_at|restart-at=s','results=s',
116            'invalidate_state|invalidate-state=s@',
117            'debug|d+','help|h|?','man|m');
118
119 pod2usage() if $options{help};
120 pod2usage({verbose=>2}) if $options{man};
121
122 my $base_dir = dirname(abs_path($0));
123
124 my $ERRORS='';
125
126 $ERRORS.="restart-at must be one of get, parse or combine\n" if
127      exists $options{restart_at} and $options{restart_at} !~ /^(?:get|parse|combine)$/;
128
129 $ERRORS.="unknown database(s)" if
130      @{$options{databases}} and
131      grep {$_ !~ /^(?:ncbi|genecard|harvester)$/i} @{$options{databases}};
132
133 if (not length $options{results}) {
134      $ERRORS.="results directory not specified";
135 }
136 elsif (not -d $options{results} or not -w $options{results}) {
137      $ERRORS.="results directory $options{results} does not exist or is not writeable";
138 }
139
140 pod2usage($ERRORS) if length $ERRORS;
141
142 if (not @{$options{databases}}) {
143      $options{databases} = [qw(ncbi genecard harvester)]
144 }
145
146 $DEBUG = $options{debug};
147
148 # There are three states for our engine
149 # Getting results
150 # Parsing them
151 # Combining results
152
153 # first, check to see if the state in the result directory exists
154
155 my %state;
156
157 $options{keywords} = [map {abs_path($_)} @{$options{keywords}}];
158
159 chdir $options{results} or die "Unable to chdir to $options{results}";
160
161 if (-e "do_it_all_state") {
162      ADVISE("Using existing state information");
163      my $state_fh = IO::File->new("do_it_all_state",'r') or die
164           "Unable to open state file for reading: $!";
165      local $/;
166      my $state_file = <$state_fh>;
167      %state = %{thaw($state_file)} or die "Unable to thaw state file";
168 }
169 else {
170      ADVISE("Starting new run");
171      %state = (keywords => [],
172                databases => [map {lc($_)} @{$options{databases}}],
173                done_keywords => {
174                                  get => {},
175                                  parse => {},
176                                  combine => {},
177                                 },
178               );
179 }
180
181 my @new_keywords;
182 if (@{$options{keywords}}) {
183      # uniqify keywords
184      my %old_keywords;
185      @old_keywords{@{$state{keywords}}} = (1) x @{$state{keywords}};
186      for my $keyword_file (@{$options{keywords}}) {
187           my $keyword_fh = IO::File->new($keyword_file,'r') or die
188                "Unable to open $keyword_file for reading: $!";
189           while (<$keyword_fh>) {
190                next if /^\s*[#;]/;
191                next unless /\w+/;
192                chomp;
193                if (not $old_keywords{$_}) {
194                     DEBUG("Adding new keyword '$_'");
195                     push @new_keywords, $_;
196                }
197                else {
198                     DEBUG("Not adding duplicate keyword '$_'");
199                }
200           }
201      }
202      push @{$state{keywords}},@new_keywords;
203 }
204
205 if (exists $options{restart_at} and length $options{restart_at}) {
206      if (lc($options{restart_at}) eq 'get') {
207           delete $state{done_keywords}{get};
208           delete $state{done_keywords}{parse};
209           delete $state{done_keywords}{combine};
210      }
211      elsif (lc($options{restart_at}) eq 'parse') {
212           delete $state{done_keywords}{parse};
213           delete $state{done_keywords}{combine};
214      }
215      elsif (lc($options{restart_at}) eq 'combine') {
216           delete $state{done_keywords}{combine};
217      }
218 }
219
220 if (exists $options{invalidate_state}) {
221      for my $invalidate_state (@{$options{invalidate_state}}) {
222           my ($method,$database,$keyword) = split /,/, $invalidate_state;
223           if (not exists $state{done_keywords}{$method}) {
224                print STDERR "Method '$method' does not exist, and cannot be invalidated\n";
225           }
226           elsif (not exists $state{done_keywords}{$method}{$database}) {
227                print STDERR "Database '$database' does not exist for method '$method', and cannot be invalidated\n";
228           }
229           elsif (not exists $state{done_keywords}{$method}{$database}{$keyword}) {
230                print STDERR "Keyword '$keyword' does not exist for database '$database' and method '$method', and cannot be invalidated\n";
231           }
232           else {
233                delete $state{done_keywords}{$method}{$database}{$keyword};
234           }
235      }
236 }
237
238 # now we need to figure out what has to happen
239 # for each keyword, we check to see if we've got results, parsed
240 # results, and combined it. If not, we queue up those actions.
241
242 my %actions = (combine => 0,
243                get     => {},
244                parse   => {},
245               );
246
247 if (not @{$state{keywords}}) {
248      ADVISE("There are no keywords specified");
249 }
250
251 for my $keyword (@{$state{keywords}}) {
252      for my $database (@{$state{databases}}) {
253           if (not exists $state{done_keywords}{get}{$database}{$keyword}) {
254                push @{$actions{get}{$database}}, $keyword;
255                delete $state{done_keywords}{parse}{$database}{$keyword} if
256                     exists $state{done_keywords}{parse}{$database}{$keyword};
257                delete $state{done_keywords}{combine}{$database}{$keyword} if
258                     exists $state{done_keywords}{combine}{$database}{$keyword};
259           }
260           if (not exists $state{done_keywords}{parse}{$database}{$keyword}) {
261                push @{$actions{parse}{$database}},$keyword;
262                delete $state{done_keywords}{combine}{$database}{$keyword} if
263                     exists $state{done_keywords}{combine}{$database}{$keyword};
264           }
265           if (not exists $state{done_keywords}{combine}{$database}{$keyword}) {
266               $actions{combine} = 1;
267           }
268      }
269 }
270
271
272 for my $state (qw(get parse)) {
273      my %databases;
274      for my $database (keys %{$actions{$state}}) {
275           next unless @{$actions{$state}{$database}};
276           $databases{$database}{queue} = Thread::Queue->new
277                or die "Unable to create new thread queue";
278           $databases{$database}{thread} = threads->create(\&handle_action,$state,$database,$databases{$database}{queue})
279                or die "Unable to create new thread";
280           $databases{$database}{queue}->enqueue(@{$actions{$state}{$database}});
281           $databases{$database}{queue}->enqueue(undef);
282      }
283      my $ERRORS=0;
284      for my $database (keys %databases) {
285           my ($actioned_keywords,$failed_keywords) = @{$databases{$database}{thread}->join||[]};
286           if (not defined $failed_keywords) {
287                ADVISE("Something bad happened during '$state' of '$database'");
288                $ERRORS = 1;
289           }
290           elsif (@{$failed_keywords}) {
291                ADVISE("These keywords failed during '$state' of '$database':",@{$failed_keywords});
292                $ERRORS=1;
293           }
294           @{$state{done_keywords}{$state}{$database}}{@{$actioned_keywords}} = (1) x @{$actioned_keywords};
295           delete @{$state{done_keywords}{$state}{$database}}{@{$failed_keywords}};
296      }
297      save_state(\%state);
298      if ($ERRORS) {
299           WARN("Stoping, as there are errors");
300           exit 1;
301      }
302 }
303
304 if ($actions{combine}) {
305      save_state(\%state);
306      # deal with combining results
307      my @parsed_results = map { my $db = $_;
308                                 map {
309                                      "parsed_results_${db}_${_}.txt"
310                                 } keys %{$state{done_keywords}{parse}{$db}}
311                            } keys %{$state{done_keywords}{parse}};
312
313      write_command_to_file('combined_results.txt',
314                            "$base_dir/combine_results",
315                            @parsed_results,
316                           );
317      for my $result (@parsed_results) {
318           $result =~ s/^parsed_results_//;
319           $result =~ s/\.txt$//;
320           my ($db,$keyword) = split /_/, $result, 2;
321           $state{done_keywords}{combined}{$db}{$keyword} = 1;
322      }
323      save_state(\%state);
324      write_command_to_file('combined_results_table.txt',
325                            "$base_dir/results_to_table",
326                            'combined_results.txt',
327                           );
328      ADVISE("Finished; results in $options{results}/combined_results.txt");
329 }
330 else {
331      ADVISE('Nothing to do. [Perhaps you wanted --restart-at?]');
332 }
333
334 sub handle_action{
335      my ($state,$database,$queue) = @_;
336      my $keyword;
337      my $actioned_keywords = [];
338      my $failed_keywords = [];
339      DEBUG("Beginning to handle actions for state '$state' database '$database'");
340      while ($keyword = $queue->dequeue) {
341           DEBUG("Handling state '$state' database '$database' keyword '$keyword'");
342           # handle the action, baybee
343           if ($state eq 'get') {
344                my $command_fh;
345                eval {
346                     open($command_fh,'|-',
347                          "$base_dir/get_${database}_results",
348                         ) or die "unable to execute '$base_dir/get_${database}_results'";
349                     print {$command_fh} "$keyword\n" or die "unable to print $keyword to 'get_${database}_results'";
350                     close($command_fh) or die "Unable to close filehandle";
351                     if ($? != 0) {
352                          die "get_${database}_results with keyword $keyword failed with error code ".($?>>8);
353                     }
354                };
355                if ($@) {
356                     WARN($@);
357                     push @{$failed_keywords}, $keyword;
358                     next;
359                }
360           }
361           elsif ($state eq 'parse') {
362                eval {
363                     write_command_to_file("parsed_results_${database}_${keyword}.txt",
364                                           "$base_dir/parse_${database}_results",
365                                           '--keywords',
366                                           $keyword,
367                                          );
368                };
369                if ($@) {
370                     WARN("parse_${database}_results failed with $@");
371                     push @{$failed_keywords}, $keyword;
372                     next;
373                }
374           }
375           else {
376                die "I don't know how to handle state $state";
377           }
378           ADVISE("$state results from '$database' for '$keyword'");
379           push @{$actioned_keywords},$keyword;
380      }
381      return [$actioned_keywords,$failed_keywords];
382 }
383
384 sub save_state{
385      my ($state) = @_;
386      my $state_fh = IO::File->new("do_it_all_state",'w') or die
387           "Unable to open state file for writing: $!";
388      print {$state_fh} freeze($state) or die "Unable to freeze state file";
389      close $state_fh or die "Unable to close state file: $!";
390 }
391
392 sub write_command_to_file{
393      my ($file,@command) = @_;
394      my $fh = IO::File->new($file,'w') or
395           die "Unable to open $file for writing: $!";
396      my $command_fh;
397      open($command_fh,'-|',
398           @command,
399          ) or die "Unable to execute $command[0] $!";
400      print {$fh} <$command_fh>;
401      close $fh;
402      close $command_fh or die "$command[0] failed with ".($?>>8);
403 }
404
405
406 sub ADVISE{
407      print STDOUT map {($_,qq(\n))} @_;
408 }
409
410 sub DEBUG{
411      print STDERR map {($_,qq(\n))} @_;
412 }
413
414
415 sub WARN {
416      print STDERR map {($_,qq(\n))} @_;
417 }
418
419 __END__