[Bio] / FigKernelPackages / P2P.pm Repository:
ViewVC logotype

Diff of /FigKernelPackages/P2P.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1.12, Fri Sep 24 19:52:25 2004 UTC revision 1.25, Mon Jan 10 22:56:49 2005 UTC
# Line 18  Line 18 
18    
19  use FIG_Config;  use FIG_Config;
20    
21    use AnyDBM_File;
22    use Fcntl;
23    
24  use strict;  use strict;
25  use Exporter;  use Exporter;
26  use base qw(Exporter);  use base qw(Exporter);
27    
28    use Time::HiRes qw( usleep ualarm gettimeofday tv_interval );
29    
30  use Data::Dumper;  use Data::Dumper;
31    
32  use vars qw(@EXPORT @EXPORT_OK);  use vars qw(@EXPORT @EXPORT_OK);
# Line 31  Line 36 
36  our $ns_p2p = "http://thefig.info/schemas/p2p_update";  our $ns_p2p = "http://thefig.info/schemas/p2p_update";
37  our $ns_relay = "http://thefig.info/schemas/p2p_relay";  our $ns_relay = "http://thefig.info/schemas/p2p_relay";
38    
39    my $peg_batch_size = 1000;
40    my $anno_batch_size = 1000;
41    my $assign_batch_size = 1000;
42    my $fin_batch_size = 1000;
43    
44    my $log_fh;
45    my $html_fh;
46    
47  =pod  =pod
48    
49  =head1 perform_update($peer)  =head1 perform_update($peer, $last_update, $skip_tough_search, $update_thru, $log_file, $html_file, $assignment_policy))
50    
51  Perform a peer-to-peer update with the given peer. $peer is an instance of  Perform a peer-to-peer update with the given peer. $peer is an instance of
52  P2P::Requestor which can connect to the peer. It is expected that the  P2P::Requestor which can connect to the peer. It is expected that the
# Line 43  Line 56 
56  This code executes the high-level protocol, maintaining state between  This code executes the high-level protocol, maintaining state between
57  calls to the peer to exchange the actual information.  calls to the peer to exchange the actual information.
58    
59        $last_update: Search for updates since this time.
60        $skip_tough_search: Do not use the time-consuming $fig->tough_search method as a last resort for peg mapping.
61        $update_thru: Search for updates until this time. Undef means to search for all since $last_update.
62        $log_file: Write logging information to this file.
63        $html_file: Write a HTML summary to this file.
64        $assignment_policy: If a list reference, contains the list of users from which we will accept assignments. If a code ref, a predicate that is passed ($peg, $timestamp, $author, $function) and returns true if the assignment should be made.
65    
66  =cut  =cut
67    
68  sub perform_update  sub perform_update
69  {  {
70      my($fig, $peer, $last_update) = @_;      my($fig, $peer, $last_update, $skip_tough_search, $update_thru, $log_file, $html_file,
71           $assignment_policy) = @_;
72    
73        my $allow_assignment;
74    
75        $log_file = "/dev/null" unless $log_file ne "";
76        open($log_fh, ">>$log_file") or die "Cannot open logfile $log_file: $!\n";
77        $log_fh->autoflush(1);
78    
79        $html_file = "/dev/null" unless $html_file ne "";
80        open($html_fh, ">$html_file") or die "Cannot open htmlfile $html_file: $!\n";
81        $html_fh->autoflush(1);
82    
83        if (ref($assignment_policy) eq "CODE")
84        {
85            $allow_assignment = $assignment_policy;
86        }
87        elsif (ref($assignment_policy) eq "ARRAY")
88        {
89            my $ahash = {};
90            map { $ahash->{$_}++; } @$assignment_policy;
91            $allow_assignment = sub {
92                return $ahash->{$_[2]};
93            };
94        }
95        elsif (ref($assignment_policy) eq "HASH")
96        {
97            $allow_assignment = sub {
98                return $assignment_policy->{$_[2]};
99            };
100        }
101        else
102        {
103            print $log_fh "Invalid assignment policy $assignment_policy\n";
104            die "Invalid assignment policy $assignment_policy\n";
105        }
106    
107        my $now = localtime();
108        my $last_str = localtime($last_update);
109        print $html_fh <<END;
110    <h1>P2P Update at $now</h1>
111    Peer URL $peer->{url}<br>
112    Update from: $last_str<br>
113    END
114    
115        print $log_fh "Beginning P2P update at $now\n";
116        print $log_fh "  Peer URL: $peer->{url}\n";
117        print $log_fh "  Update from: $last_str\n";
118        print $log_fh "\n";
119    
120      my $ret = $peer->request_update($last_update);      my $ret = $peer->request_update($last_update, $update_thru);
121    
122      if (!$ret or ref($ret) ne "ARRAY")      if (!$ret or ref($ret) ne "ARRAY")
123      {      {
124          die "perform_update: request_updated failed\n";          die "perform_update: request_update failed\n";
125      }      }
126    
127      my($session, $target_release, $num_annos, $num_pegs, $num_genomes,      my($session, $target_release, $num_assignments, $num_annos, $num_pegs, $num_genomes,
128         $target_time, $compatible) = @$ret;         $target_time, $compatible) = @$ret;
129    
130      print "perform_update: session=$session target=$target_release num_annos=$num_annos\n";      print "perform_update: session=$session target=@$target_release num_annos=$num_annos\n";
131      print "                num_pegs=$num_pegs num_genomes=$num_genomes target_time=$target_time compat=$compatible\n";      print "                num_pegs=$num_pegs num_genomes=$num_genomes target_time=$target_time compat=$compatible\n";
132    
133        my @my_release = $fig->get_release_info();
134    
135        print $log_fh "Session id = $session\n";
136        print $log_fh "Target release information: \n\t", join("\n\t", @$target_release), "\n";
137        print $log_fh "My release information: \n\t", join("\n\t", @my_release), "\n";
138        print $log_fh "$num_annos annotations\n";
139        print $log_fh "$num_assignments assignments\n";
140        print $log_fh "$num_pegs pegs\n";
141    
142        print $html_fh "Session id = $session<br>\n";
143        print $html_fh "Target release information: <br>\n\t", join("<br>\n\t", @$target_release), "<br>\n";
144        print $html_fh "My release information: <br>\n\t", join("<br>\n\t", @my_release), "<br>\n";
145        print $html_fh "$num_annos annotations<br>\n";
146        print $html_fh "$num_assignments assignments<br>\n";
147        print $html_fh "$num_pegs pegs<br>\n";
148    
149        #
150        # We now know the data release for our peer.
151        #
152        # Open up the peg translation cache database (a AnyDBM_File) tied
153        # to %peg_cache. We needn't worry about keeping it in a directory
154        # based on our current release, as it the cache directory is kept *in*
155        # the current data release directory.
156        #
157    
158        my $cache_handle;
159        my %peg_cache;
160        if ($target_release->[1] ne "")
161        {
162            my $cache_file = "pegcache.$target_release->[1].db";
163            my $cache_dir = "$FIG_Config::data/P2PQueue";
164            $fig->verify_dir($cache_dir);
165    
166            $cache_handle = tie(%peg_cache, "AnyDBM_File", "$cache_dir/$cache_file",
167                                O_CREAT | O_RDWR, 0666);
168            $cache_handle or warn "Could not tie peg_cache to $cache_dir/$cache_file: $!\n";
169        }
170    
171        #
172        # peg_mapping is the local mapping from remote->local peg. This might
173        # be replacable by peg_cache from above.
174        #
175        my %peg_mapping;
176    
177    
178      #      #
179      # We have  the information now to begin the update process. Retrieve the pegs.      # We have  the information now to begin the update process. Retrieve the pegs.
180      #      #
181    
182      $ret = $peer->get_pegs($session, 0, $num_pegs);      _compute_peg_mapping($fig, $peer, $session, $num_pegs, \%peg_mapping, \%peg_cache, $cache_handle,
183                             $skip_tough_search);
184    
185      if (!$ret or ref($ret) ne "ARRAY")      eval { $cache_handle->sync();};
186        untie %peg_cache;
187    
188        #
189        # Create a list of locally-mapped annotations on a per-genome
190        # basis.
191        #
192    
193        my %genome_annos;
194    
195        #
196        # %genome_assignments is a hash mapping from genome to a hashref
197        # that maps  peg to function (since assignments are unique).
198        #
199        # (Hm. Unless two remote pegs map to the same local peg; unclear what to do
200        # then. Punt for now).
201        #
202        my %genome_assignments;
203    
204        #
205        # Retrieve the annotations, and generate a list of mapped annotations.
206        #
207    
208        for (my $anno_start = 0; $anno_start < $num_annos; $anno_start += $anno_batch_size)
209      {      {
210          die "perform_update: get_pegs failed\n";          my $anno_req_len = $num_annos - $anno_start;
211            $anno_req_len = $anno_batch_size if $anno_req_len > $anno_batch_size;
212    
213            print "Retrieve $anno_req_len annos at $anno_start\n";
214            print $log_fh "Retrieve $anno_req_len annos at $anno_start\n";
215    
216            my $annos = $peer->get_annotations($session, $anno_start, $anno_req_len);
217    
218            for my $anno (@$annos)
219            {
220                my($his_id, $ts, $author, $anno) = @$anno;
221    
222                my $my_id = $peg_mapping{$his_id};
223                next unless $my_id;
224    
225                my $genome = $fig->genome_of($my_id);
226    
227                push(@{$genome_annos{$genome}}, [$my_id, $ts, $author, $anno]);
228            }
229      }      }
230    
231      my($peg_list, $genome_list) = @$ret;      #
232        # Do the same for the assignments
233        #
234    
235        # print Dumper($assignments);
236    
237    
238        for (my $assign_start = 0; $assign_start < $num_assignments; $assign_start += $assign_batch_size)
239        {
240            my $assign_req_len = $num_assignments - $assign_start;
241            $assign_req_len = $assign_batch_size if $assign_req_len > $assign_batch_size;
242    
243            print "Retrieve $assign_req_len assigns at $assign_start\n";
244            print $log_fh "Retrieve $assign_req_len assigns at $assign_start\n";
245    
246            my $assignments = $peer->get_assignments($session, $assign_start, $assign_req_len);
247    
248            for my $assign (@$assignments)
249            {
250                my($his_id, $ts, $author, $func) = @$assign;
251    
252                my $my_id = $peg_mapping{$his_id};
253                next unless $my_id;
254    
255                my $genome = $fig->genome_of($my_id);
256    
257                $genome_assignments{$genome}->{$my_id} =  [$my_id, $ts, $author, $func];
258            }
259        }
260    
261        # print Dumper(\%genome_annos);
262    
263      #      #
264      # Walk the peg-list to and generate @pegs_to_finalize.      # Now install annotations.
265      #      #
266    
267      my(%peg_mapping, %genome_map );      for my $genome (keys(%genome_annos))
268        {
269            #
270            # Plan:  Apply the merge_annotations.pl logic. Read the annotations
271            # from the per-org annotations file, add the new ones here, sort, and remove duplicates.
272            # Write the results to the annotations file.
273            #
274            # When we are all done, rerun the index_annotations script.
275            #
276            # Why not do that incrementally? Partly because the annotation_seeks table doesn't
277            # have a column for the genome id, so a removal of old data would require a
278            # string-match query; since a complete reindex of the annotations is pretty
279            # fast (60 sec on a G4 laptop on a firewire disk), it's not clear whether the incremental
280            # update would actually be a win.
281            #
282    
283      for my $peg_info (@$peg_list)          my @annos = @{$genome_annos{$genome}};
284            my $assignments = $genome_assignments{$genome};
285            #
286            # %assignment_annos is a hash from peg to the list
287            # of annotations for that peg.
288            #
289            my %assignment_annos;
290    
291            my $dir = "$FIG_Config::organisms/$genome";
292            my $anno_file = "$dir/annotations";
293            my $anno_bak = "$dir/annotations." . time;
294    
295            my $new_count = @annos;
296    
297            #
298            # Rename the annotations file to a new name based on the current time.
299            #
300    
301            my $gs = $fig->genus_species($genome);
302            print $html_fh "<h1>Updates for $genome ($gs)</h1>\n";
303    
304            if (-f $anno_file)
305      {      {
306          my($key, $peg, @rest) = @$peg_info;              rename($anno_file, $anno_bak) or die "Cannot rename $anno_file to $anno_bak: $!";
307                print $log_fh "Moved annotations file $anno_file to backup $anno_bak\n";
308            }
309    
310          if ($key eq 'peg')          if (open(my $fh, "<$anno_bak"))
311          {          {
312              #              #
313              # Peg id is directly usable.              # While we are scanning here, we look for the latest local assignment
314                # for any peg for which we are installing an assignment.
315              #              #
316              $peg_mapping{$peg} = $peg;              local($/) = "\n//\n";
317    
318                my($chunk, $peg, $ts, $author, $anno);
319    
320                while (defined($chunk = <$fh>))
321                {
322                    chomp $chunk;
323                    ($peg, $ts, $author, $anno) = split(/\n/, $chunk, 4);
324    
325                    if ($peg =~ /^fig\|/ and $ts =~ /^\d+$/)
326                    {
327                        my $ent = [$peg, $ts, $author, $anno];
328                        push(@annos, $ent);
329    
330                        if (defined($assignments->{$peg}))
331                        {
332                            #
333                            # We have an incoming assignment for this peg.
334                            # Don't parse anything yet, but push the annotation
335                            # on a list so we can sort by date.
336                            #
337                            push(@{$assignment_annos{$peg}}, $ent);
338          }          }
339          elsif ($key eq 'peg_info')                  }
340                }
341                close($fh);
342            }
343    
344            #
345            # Determine if we are going to install an assignment.
346            #
347    
348            my $cgi_url = &FIG::cgi_url();
349            print $html_fh "<h2>Assignments made</h2>\n";
350            print $html_fh "<table border=\"1\">\n";
351            print $html_fh "<tr><th>PEG</th><th>Old assignment</th><th>New assignment</th><tr>\n";
352    
353            for my $peg (keys %$assignments)
354          {          {
355                my(undef, $ts, $author, $func) = @{$assignments->{$peg}};
356    
357                #
358                # Sort the existing annotations for this peg by date.
359              #              #
360              # Peg id not directly usable.              # Recall that this list has entries [$peg, $timestamp, $author, $anno]
361              #              #
362    
363              my($alias_list, $genome_id) = @rest;              my @eannos;
364                if (ref($assignment_annos{$peg}))
365                {
366                    @eannos = sort { $b->[1] <=> $a->[1] } @{$assignment_annos{$peg}};
367                }
368                else
369                {
370                    #
371                    # No assignment annotations found.
372                    #
373                    @eannos = ();
374                }
375    
376              for my $alias (@$alias_list)              # print "Assignment annos for $peg: ", Dumper(\@eannos);
377    
378                #
379                # Filter out just the master assignments that are newer than
380                # the one we are contemplating putting in place.
381                #
382    
383                my @cand = grep {
384                    ($_->[1] > $ts) and ($_->[3] =~ /Set master function to/)
385                    } @eannos;
386    
387                if (@cand > 0)
388              {              {
389                  my $mapped = $fig->by_alias($alias);                  #
390                  if ($mapped)                  # Here is were some policy needs to be put in place --
391                    # we have a more recent annotation on the current system.
392                    #
393                    # For now, we will not install an assignment if there is any
394                    # newer assignment in place.
395                    #
396    
397                    warn "Skipping assignment for $peg $func due to more recent assignment $cand[0]->[3]\n";
398                    print $log_fh "Skipping assignment for $peg $func due to more recent assignment $cand[0]->[3]\n";
399                }
400                else
401                  {                  {
402                      print "$peg maps to $mapped via $alias\n";                  #
403                      $peg_mapping{$peg}= $mapped;                  # Nothing is blocking us. While we are testing, just slam this assignment in.
404                      last;                  #
405    
406                    my $old = $fig->function_of($peg, 'master');
407    
408                    if ($old ne $func and &$allow_assignment($peg, $ts, $author, $func))
409                    {
410                        my $l = "$cgi_url/protein.cgi?prot=$peg";
411                        print $html_fh "<tr><td><a href=\"$l\">$peg</a></td><td>$old</td><td>$func</td></tr>\n";
412    
413                        print "Assign $peg $func\n";
414                        print $log_fh "Assign $peg $func\n";
415                        print $log_fh "   was $old\n";
416                        $fig->assign_function($peg, 'master', $func);
417    
418                    }
419                  }                  }
420              }              }
421    
422            print $html_fh "</table>\n";
423    
424            print $html_fh "<h2>Annotations added</h2>\n";
425            print $html_fh "<table border=\"1\">\n";
426            print $html_fh "<tr><th>PEG</th><th>Time</th><th>Author</th><th>Annotation</th></tr>\n";
427    
428            open(my $outfh, ">$anno_file") or die "Cannot open new annotation file $anno_file: $!\n";
429    
430            my $last;
431            my @sorted = sort { ($a->[0] cmp $b->[0]) or ($a->[1] <=> $b->[1]) } @annos;
432            my $inst = 0;
433            my $dup = 0;
434            foreach my $ann (@sorted)
435            {
436                my $txt = join("\n", @$ann);
437              #              #
438              # If we didn't succeed in mapping by alias,              # Drop the trailing \n if there is one; we  will add it back when we print and
439              # stash this in the list of pegs to be mapped by              # want to ensure the file format remains sane.
             # genome.  
440              #              #
441                chomp $txt;
442                if ($txt ne $last)
443                {
444                    my $peg = $ann->[0];
445                    my $l = "$cgi_url/protein.cgi?prot=$peg";
446                    print $html_fh "<tr>" . join("\n", map { "<td>$_</td>" }
447                                                 "<a href=\"$l\">$peg</a>",
448                                                 scalar(localtime($ann->[1])), $ann->[2], $ann->[3])
449                        . "</tr>\n";
450    
451              if (!defined($peg_mapping{$peg}))                  print $outfh "$txt\n//\n";
452                    $last = $txt;
453                    # print "Inst $ann->[0] $ann->[1] $ann->[2]\n";
454                    $inst++;
455                }
456                else
457              {              {
458                  push(@{$genome_map{$genome_id}}, $peg);                  # print "Dup $ann->[0] $ann->[1] $ann->[2]\n";
459                  print "$peg did not map\n";                  $dup++;
460              }              }
461          }          }
462            print $html_fh "</table>\n";
463            close($outfh);
464            chmod(0666, $anno_file) or warn "Cannot chmod 0666 $anno_file: $!\n";
465            print "Wrote $anno_file. $new_count new annos, $inst installed, $dup duplicates\n";
466            print $log_fh "Wrote $anno_file. $new_count new annos, $inst installed, $dup duplicates\n";
467        }
468        close($html_fh);
469      }      }
470    
471      #      #
472      # finished first pass. Now go over the per-genome mappings that need to be made.  # Compute the peg mapping for a session.
473    #
474    # $fig          Active FIG instance
475    # $peer         P2P peer for this session.
476    # $session      P2P session ID
477    # $peg_mapping  Hash ref for the remote -> local PEG mapping
478    # $peg_cache    Hash ref for the persistent remote -> local PEG mapping cache db.
479    # $cache_handle AnyDBM_File handle corresponding to $peg_cache.
480    #
481    sub _compute_peg_mapping
482    {
483        my($fig, $peer, $session, $num_pegs, $peg_mapping, $peg_cache, $cache_handle, $skip_tough_search) = @_;
484    
485        #
486        # genome_map is a hash mapping from target genome id to a list of
487        # pegs on the target. This is used to construct a finalize_pegs request after
488        # the first phase of peg mapping.
489        #
490    
491        my %genome_map;
492    
493        #
494        # target_genome_info is a hash mapping from target genome
495        # identifier to the target-side information on the genome -
496        # number of contigs, number of nucleotides, checksum.
497        #
498        # We accumulate it here across possibly multiple batches of
499        # peg retrievals in order to create a single  finalization
500        # list.
501        #
502    
503        my %target_genome_info;
504    
505        #
506        # For very large transfers, we need to batch the peg processing.
507        #
508    
509        for (my $peg_start = 0; $peg_start < $num_pegs; $peg_start += $peg_batch_size)
510        {
511            my $peg_req_len = $num_pegs - $peg_start;
512            $peg_req_len = $peg_batch_size if $peg_req_len > $peg_batch_size;
513    
514            print "Getting $peg_req_len pegs at $peg_start\n";
515            print $log_fh "Getting $peg_req_len pegs at $peg_start\n";
516            my $ret = $peer->get_pegs($session, $peg_start, $peg_req_len);
517    
518            if (!$ret or ref($ret) ne "ARRAY")
519            {
520                die "perform_update: get_pegs failed\n";
521            }
522    
523            my($peg_list, $genome_list) = @$ret;
524    
525            for my $gent (@$genome_list)
526            {
527                $target_genome_info{$gent->[0]} = $gent;
528            }
529    
530            _compute_peg_mapping_batch($fig, $peer, $session, $peg_mapping, $peg_cache, $cache_handle,
531                                       $peg_list, \%genome_map);
532        }
533    
534        #
535        # We have finished first pass. Now go over the per-genome mappings that need to be made.
536      #      #
537      # $genome_map{$genome_id} is a list of pegs that reside on that genome.      # $genome_map{$genome_id} is a list of pegs that reside on that genome.
538      # the pegs and genome id are both target-based identifiers.      # The pegs and genome id are both target-based identifiers.
539        #
540        # %target_genome_info defines the list of genome information we have on the remote
541        # side.
542        #
543        # We build a request to be passed to finalize_pegs. Each entry in the request is either
544        # ['peg_genome', $peg] which means that we have a genome that corresponds to the
545        # genome the peg is in. We can attempt to map via contig locations.
546        #
547        # If that is not the case,  we pass a request entry of ['peg_unknown', $peg]
548        # which will result in the sequence data being returned.
549      #      #
550    
551      my @finalize_req = ();      my @finalize_req = ();
552    
553        #
554        # local_genome maps a target peg identifier to the local genome id it translates to.
555        #
556      my %local_genome;      my %local_genome;
557    
558      for my $genome_info (@$genome_list)      for my $genome (keys(%target_genome_info))
559      {      {
560          my($genome, $n_contigs, $n_nucs, $cksum) = @$genome_info;          my($tg, $n_contigs, $n_nucs, $cksum) = @{$target_genome_info{$genome}};
561    
562            $tg eq $genome or die "Invalid entry in target_genome_info for $genome => $tg, $n_contigs, $n_nucs, $cksum";
563    
564            #
565            # Don't bother unless we have any pegs to look up.
566            #
567          next unless defined($genome_map{$genome});          next unless defined($genome_map{$genome});
568    
569          #          #
# Line 159  Line 584 
584              #              #
585    
586              print "$genome mapped to $my_genome\n";              print "$genome mapped to $my_genome\n";
587                print $log_fh "$genome mapped to $my_genome\n";
588              for my $peg (@$pegs)              for my $peg (@$pegs)
589              {              {
590                  push(@finalize_req, ['peg_genome', $peg]);                  push(@finalize_req, ['peg_genome', $peg]);
# Line 177  Line 603 
603      }      }
604    
605      #      #
606      # If we need to finalize, make the call.      # We've built our finalization request. Handle it (possibly with batching here too).
607      if (@finalize_req)      #
608    
609        _process_finalization_request($fig, $peer, $session, $peg_mapping, $peg_cache, $cache_handle,
610                                     \%local_genome, \@finalize_req, $skip_tough_search);
611    
612    }
613    
614    #
615    # Process one batch of PEGs.
616    #
617    # Same args as _compute_peg_mapping, with the addition of:
618    #
619    #       $peg_list       List of pegs to be processed
620    #       $genome_map     Hash maintaining list of genomes with their pegs.
621    #       $target_genome_info     Hash maintaining overall list of target-side genome information.
622    #
623    sub _compute_peg_mapping_batch
624    {
625        my($fig, $peer, $session, $peg_mapping, $peg_cache, $cache_handle,
626           $peg_list, $genome_map, $target_genome_info) = @_;
627    
628        #
629        # Walk the list of pegs as returned from get_pegs() and determine what has to
630        # be done.
631        #
632        # If the entry is ['peg', $peg], we can use the peg ID as is.
633        #
634        # If the entry is ['peg_info', $peg, $alias_list, $genome], the peg
635        # has the given aliases, and is in the given genome.
636        #
637        for my $peg_info (@$peg_list)
638        {
639            my($key, $peg, @rest) = @$peg_info;
640    
641            if ($key eq 'peg')
642            {
643                #
644                # Peg id is directly usable.
645                #
646                $peg_mapping->{$peg} = $peg;
647            }
648            elsif ($key eq 'peg_info')
649            {
650                #
651                # Peg id not directly usable. See if we have it in the cache.
652                #
653    
654                if ((my $cached = $peg_cache->{$peg}) ne "")
655                {
656                    #
657                    # Cool, we've cached the result. Use it.
658                    #
659    
660                    $peg_mapping->{$peg} = $cached;
661                    # warn "Found cached mapping $peg => $cached\n";
662                    next;
663                }
664    
665                #
666                # It is not cached. Attempt to resolve by means of alias IDs.
667                #
668    
669                my($alias_list, $genome_id) = @rest;
670    
671                for my $alias (@$alias_list)
672                {
673                    my $mapped = $fig->by_alias($alias);
674                    if ($mapped)
675                    {
676                        print "$peg maps to $mapped via $alias\n";
677                        print $log_fh "$peg maps to $mapped via $alias\n";
678                        $peg_mapping->{$peg}= $mapped;
679                        $peg_cache->{$peg} = $mapped;
680                        last;
681                    }
682                }
683    
684                #
685                # If we weren't able to resolve by ID,
686                # add to %genome_map as a PEG that will need
687                # to be resolved by means of contig location.
688                #
689    
690                if (!defined($peg_mapping->{$peg}))
691                {
692                    push(@{$genome_map->{$genome_id}}, $peg);
693                    print "$peg did not map on first pass\n";
694                    print $log_fh "$peg did not map on first pass\n";
695                }
696            }
697        }
698    
699        #
700        # Flush the cache to write out any computed mappings.
701        #
702        eval { $cache_handle->sync();};
703    
704    }
705    
706    sub _process_finalization_request
707    {
708        my($fig, $peer, $session, $peg_mapping, $peg_cache, $cache_handle,
709           $local_genome, $finalize_req, $skip_tough_search) = @_;
710    
711        #
712        # Immediately return unless there's something to do.
713        #
714        return unless ref($finalize_req) and @$finalize_req > 0;
715    
716        while (@$finalize_req > 0)
717      {      {
718          print Dumper(\@finalize_req);          my @req = splice(@$finalize_req, 0, $fin_batch_size);
719          $ret = $peer->finalize_pegs($session, \@finalize_req);  
720            print "Invoking finalize_pegs on ", int(@req), " pegs\n";
721            print $log_fh "Invoking finalize_pegs on ", int(@req), " pegs\n";
722            my $ret = $peer->finalize_pegs($session, \@req);
723    
724          if (!$ret or ref($ret) ne "ARRAY")          if (!$ret or ref($ret) ne "ARRAY")
725          {          {
# Line 193  Line 731 
731          # sequence data. Attempt to finish up the mapping.          # sequence data. Attempt to finish up the mapping.
732          #          #
733    
734            my(%sought, %sought_seq);
735    
736    
737          my $dbh = $fig->db_handle();          my $dbh = $fig->db_handle();
738          for my $entry (@$ret)          for my $entry (@$ret)
# Line 201  Line 741 
741    
742              if ($what eq "peg_loc")              if ($what eq "peg_loc")
743              {              {
744                  my($strand, $start, $end, $cksum) = @rest;                  my($strand, $start, $end, $cksum, $seq) = @rest;
745    
746                  #                  #
747                  # We have a contig location. Try to find a matching contig                  # We have a contig location. Try to find a matching contig
748                  # here, and see if it maps to something.                  # here, and see if it maps to something.
749                  #                  #
750    
751                  my $my_genome = $local_genome{$peg};                  my $my_genome = $local_genome->{$peg};
752                  my $local_contig = $fig->find_contig_with_checksum($my_genome, $cksum);                  my $local_contig = $fig->find_contig_with_checksum($my_genome, $cksum);
753                  if ($local_contig)                  if ($local_contig)
754                  {                  {
# Line 228  Line 768 
768                      {                      {
769                          my(@ids) = map { $_->[0] } @$res;                          my(@ids) = map { $_->[0] } @$res;
770                          my $id = $ids[0];                          my $id = $ids[0];
771                          $peg_mapping{$peg} = $id;                          $peg_mapping->{$peg} = $id;
772                            $peg_cache->{$peg} = $id;
773                          print "Mapped $peg to $id via contigs\n";                          print "Mapped $peg to $id via contigs\n";
774                          if (@$res > 1)                          if (@$res > 1)
775                          {                          {
776                              warn "Multiple mappings found for $peg: @ids\n";                              warn "Multiple mappings found for $peg: @ids\n";
777                                print $log_fh "Multiple mappings found for $peg: @ids\n";
778                          }                          }
779                      }                      }
780                      else                      else
781                      {                      {
782                          print "failed: $peg  $my_genome and contig $local_contig start=$start end=$end strand=$strand\n";                          print "failed: $peg  $my_genome and contig $local_contig start=$start end=$end strand=$strand\n";
783                            print $log_fh "failed: $peg  $my_genome and contig $local_contig start=$start end=$end strand=$strand\n";
784                            print $html_fh "Contig match failed: $peg $my_genome contig $local_contig start $start end $end strand $strand<br>\n";
785                            $sought{$peg}++;
786                            $sought_seq{$peg} = $seq;
787                      }                      }
788                  }                  }
789                  else                  else
790                  {                  {
791                      print "Mapping failed for $my_genome checksum $cksum\n";                      print "Mapping failed for $my_genome checksum $cksum\n";
792                        print $log_fh "Mapping failed for $my_genome checksum $cksum\n";
793                        print $html_fh "Mapping failed for $my_genome checksum $cksum<br>\n";
794                        $sought{$peg}++;
795                        $sought_seq{$peg} = $seq;
796                  }                  }
797              }              }
798          }              elsif ($what eq "peg_seq")
799                {
800                    my($seq) = @rest;
801    
802                    $sought{$peg}++;
803                    $sought_seq{$peg} = $seq;
804      }      }
805  }  }
806    
807            #
808            # Now see if we need to do a tough search.
809            #
810    
811            if (keys(%sought) > 0 and !$skip_tough_search)
812            {
813                my %trans;
814    
815                print "Starting tough search\n";
816                print $log_fh "Starting tough search\n";
817    
818                $fig->tough_search(undef, \%sought_seq, \%trans, \%sought);
819                print "Tough search translated: \n";
820                print $log_fh "Tough search translated: \n";
821                while (my($tpeg, $ttrans) = each(%trans))
822                {
823                    print "  $tpeg -> $ttrans\n";
824                    print $log_fh "  $tpeg -> $ttrans\n";
825                    $peg_mapping->{$tpeg} = $ttrans;
826                    $peg_cache->{$tpeg} = $ttrans;
827                }
828            }
829        }
830    }
831    
832  #############  #############
833  #  #
# Line 269  Line 848 
848  {  {
849      my($class, $url) = @_;      my($class, $url) = @_;
850    
851      my $proxy = SOAP::Lite->uri($P2P::ns_relay)->proxy($url);      my $creds = [];
852    
853        my $proxy = SOAP::Lite->uri($P2P::ns_relay)->proxy([$url,
854                                                            credentials => $creds]);
855    
856      my $self = {      my $self = {
857          url => $url,          url => $url,
# Line 342  Line 924 
924          # element in the body of the message.          # element in the body of the message.
925          #          #
926          my $ns = $reply->namespaceuriof('/Envelope/Body/[1]');          my $ns = $reply->namespaceuriof('/Envelope/Body/[1]');
927          print "Reply ns=$ns want $P2P::ns_relay\n";          # print "Reply ns=$ns want $P2P::ns_relay\n";
928    
929          if ($ns eq $P2P::ns_relay)          if ($ns eq $P2P::ns_relay)
930          {          {
931              my $val = $reply->result;              my $val = $reply->result;
932              print "got val=", Dumper($val);              # print "got val=", Dumper($val);
933              if ($val->[0] eq 'deferred')              if ($val->[0] eq 'deferred')
934              {              {
935                  #                  #
# Line 389  Line 971 
971  use strict;  use strict;
972    
973  use Data::Dumper;  use Data::Dumper;
974    use Time::HiRes qw( usleep ualarm gettimeofday tv_interval );
975    
976  use SOAP::Lite;  use SOAP::Lite;
977    
978    #use SOAP::Lite +trace => [qw(transport dispatch result debug)];
979  use P2P;  use P2P;
980    
981  #  #
# Line 400  Line 985 
985    
986  sub new  sub new
987  {  {
988      my($class, $fig, $url, $peer_id, $relay) = @_;      my($class, $fig, $url, $peer_id, $relay, $credentials) = @_;
989    
990        $credentials = [] unless ref($credentials);
991    
992      my $proxy = SOAP::Lite->uri($ns_p2p)->proxy($url);      my $proxy = SOAP::Lite->uri($ns_p2p)->proxy($url, timeout => 3600,
993                                                    credentials => $credentials);
994    
995      my $self = {      my $self = {
996          fig => $fig,          fig => $fig,
# Line 428  Line 1016 
1016    
1017  sub request_update  sub request_update
1018  {  {
1019      my($self, $last_update) = @_;      my($self, $last_update, $update_thru) = @_;
1020    
1021      my $rel = $self->{fig}->get_release_info();      my $rel = [$self->{fig}->get_release_info()];
1022    
1023      if (!defined($last_update))      if (!defined($last_update))
1024      {      {
1025          $last_update = $self->{fig}->get_peer_last_update($self->{peer_id});          $last_update = $self->{fig}->get_peer_last_update($self->{peer_id});
1026      }      }
1027    
1028      my $reply = $self->{proxy}->request_update($rel, $last_update);      print "Requesting update via $self->{proxy}\n";
1029        my $reply = $self->{proxy}->request_update($rel, $last_update, $update_thru);
1030        # print "Got reply ", Dumper($reply);
1031    
1032      if ($self->{relay})      if ($self->{relay})
1033      {      {
# Line 474  Line 1064 
1064      return $self->call("finalize_pegs", $session_id, $request);      return $self->call("finalize_pegs", $session_id, $request);
1065  }  }
1066    
1067    sub get_annotations
1068    {
1069        my($self, $session_id, $start, $length) = @_;
1070    
1071        return $self->call("get_annotations", $session_id, $start, $length);
1072    }
1073    
1074    sub get_assignments
1075    {
1076        my($self, $session_id, $start, $length) = @_;
1077    
1078        return $self->call("get_assignments", $session_id, $start, $length);
1079    }
1080    
1081  sub call  sub call
1082  {  {
1083      my($self, $func, @args) = @_;      my($self, $func, @args) = @_;
1084    
1085        my $t0 = [gettimeofday()];
1086        print "Calling $func\n";
1087      my $reply = $self->{proxy}->$func(@args);      my $reply = $self->{proxy}->$func(@args);
1088        my $t1 = [gettimeofday()];
1089    
1090        my $elap = tv_interval($t0, $t1);
1091        print "Call to $func took $elap\n";
1092    
1093      if ($self->{relay})      if ($self->{relay})
1094      {      {
# Line 516  Line 1126 
1126    
1127  sub request_update  sub request_update
1128  {  {
1129      my($class, $his_release, $last_update)= @_;      my($class, $his_release, $last_update, $update_thru)= @_;
1130    
1131      #      #
1132      # Verify input.      # Verify input.
# Line 527  Line 1137 
1137          die "request_update: last_update must be a number (not '$last_update')\n";          die "request_update: last_update must be a number (not '$last_update')\n";
1138      }      }
1139    
1140        if ($update_thru eq "")
1141        {
1142            $update_thru = time + 10000;
1143        }
1144    
1145      #      #
1146      # Create a new session id and a spool directory to use for storage      # Create a new session id and a spool directory to use for storage
1147      # of information about it. This can go in the tempdir since it is      # of information about it. This can go in the tempdir since it is
# Line 534  Line 1149 
1149      #      #
1150    
1151      &FIG::verify_dir("$FIG_Config::temp/p2p_spool");      &FIG::verify_dir("$FIG_Config::temp/p2p_spool");
1152      #my $spool_dir = tempdir(DIR  => "$FIG_Config::temp/p2p_spool");      my $spool_dir = tempdir(DIR  => "$FIG_Config::temp/p2p_spool");
1153    
1154      my $spool_dir = "$FIG_Config::temp/p2p_spool/test";      #my $spool_dir = "$FIG_Config::temp/p2p_spool/test";
1155      &FIG::verify_dir($spool_dir);      &FIG::verify_dir($spool_dir);
1156    
1157      my $session_id = basename($spool_dir);      my $session_id = basename($spool_dir);
# Line 554  Line 1169 
1169    
1170      my %pegs;      my %pegs;
1171    
1172        #
1173        # We keep track of usernames that have been seen, so that
1174        # we can both update our local user database and
1175        # we can report them to our peer.
1176        #
1177    
1178        my %users;
1179    
1180      my $num_annos = 0;      my $num_annos = 0;
1181      my $num_genomes = 0;      my $num_genomes = 0;
1182      my $num_pegs = 0;      my $num_pegs = 0;
1183        my $num_assignments = 0;
1184    
1185      my $anno_fh;      my $anno_fh;
1186      open($anno_fh, ">$spool_dir/annos");      open($anno_fh, ">$spool_dir/annos");
# Line 567  Line 1191 
1191      my $genome_fh;      my $genome_fh;
1192      open($genome_fh, ">$spool_dir/genomes");      open($genome_fh, ">$spool_dir/genomes");
1193    
1194        my $assign_fh;
1195        open($assign_fh, ">$spool_dir/assignments");
1196    
1197        #
1198        # We originally used a query to get the PEGs that needed to have annotations
1199        # sent. Unfortunately, this performed very poorly due to all of the resultant
1200        # seeking around in the annotations files.
1201        #
1202        # The code below just runs through all of the anno files looking for annos.
1203        #
1204        # A better way to do this would be to do a query to retrieve the genome id's for
1205        # genomes that have updates. The problem here is that the annotation_seeks
1206        # table doesn't have an explicit genome field.
1207        #
1208        # Surprisingly, to me anyway, the following query appers to run quickly, in both
1209        # postgres and mysql:
1210        #
1211        # SELECT distinct(substring(fid from 5 for position('.peg.' in fid) - 5))
1212        # FROM annotation_seeks
1213        # WHERE dateof > some-date.
1214        #
1215        # The output of that can be parsed to get the genome id and just those
1216        # annotations files searched.
1217        #
1218    
1219      for my $genome (@$all_genomes)      for my $genome (@$all_genomes)
1220      {      {
1221          my $num_annos_for_genome = 0;          my $num_annos_for_genome = 0;
1222            my %assignment;
1223    
1224          my $genome_dir = "$FIG_Config::organisms/$genome";          my $genome_dir = "$FIG_Config::organisms/$genome";
1225          next unless -d $genome_dir;          next unless -d $genome_dir;
# Line 586  Line 1236 
1236    
1237                  if ((($fid, $anno_time, $who, $anno_text) =                  if ((($fid, $anno_time, $who, $anno_text) =
1238                       ($ann =~ /^(fig\|\d+\.\d+\.peg\.\d+)\n(\d+)\n(\S+)\n(.*\S)/s)) and                       ($ann =~ /^(fig\|\d+\.\d+\.peg\.\d+)\n(\d+)\n(\S+)\n(.*\S)/s)) and
1239                      $anno_time > $last_update)                      $anno_time > $last_update and
1240                        $anno_time < $update_thru)
1241    
1242                  {                  {
1243                      #                      #
1244                        # Update users list.
1245                        #
1246    
1247                        $users{$who}++;
1248    
1249                        #
1250                      # Look up aliases if we haven't seen this fid before.                      # Look up aliases if we haven't seen this fid before.
1251                      #                      #
1252    
# Line 607  Line 1264 
1264    
1265                      $num_annos_for_genome++;                      $num_annos_for_genome++;
1266                      $num_annos++;                      $num_annos++;
1267    
1268                        #
1269                        # While we're here, see if this is an assignment. We check in the
1270                        # %assignment hash, which is keyed on fid, to see if we already
1271                        # saw an assignment for this fid. If we have, we keep this one only if
1272                        # the assignment time on it is later than the one we saw already.
1273                        #
1274                        # We are only looking at master assignments for now. We will need
1275                        # to return to this issue and reexamine it, but in order to move
1276                        # forward I am only matching master assignments.
1277                        #
1278    
1279                        if ($anno_text =~ /Set master function to\n(\S[^\n]+\S)/)
1280                        {
1281                            my $func = $1;
1282    
1283                            my $other = $assignment{$fid};
1284    
1285                            #
1286                            # If we haven't seen an assignment for this fid,
1287                            # or if it the other assignment has a timestamp that
1288                            # is earlier than this one, set the assignment.
1289                            #
1290    
1291                            if (!defined($other) or
1292                                ($other->[1] < $anno_time))
1293                            {
1294                                $assignment{$fid} = [$fid, $anno_time, $who, $func];
1295                            }
1296                        }
1297                  }                  }
1298              }              }
1299              close($afh);              close($afh);
1300    
1301                #
1302                # Write out the assignments that remain.
1303                #
1304    
1305                for my $fid (sort keys(%assignment))
1306                {
1307                    print $assign_fh join("\t", @{$assignment{$fid}}), "\n";
1308                    $num_assignments++;
1309          }          }
1310            }
1311    
1312    
1313          #          #
1314          # Determine genome information if we have annotations for this one.          # Determine genome information if we have annotations for this one.
# Line 643  Line 1341 
1341      close($anno_fh);      close($anno_fh);
1342      close($peg_fh);      close($peg_fh);
1343      close($genome_fh);      close($genome_fh);
1344        close($assign_fh);
1345    
1346      print "Pegs: $num_pegs\n";      print "Pegs: $num_pegs\n";
1347      print "Genomes: $num_genomes\n";      print "Genomes: $num_genomes\n";
# Line 652  Line 1351 
1351      # Check compatibility.      # Check compatibility.
1352      #      #
1353    
1354      my $my_release = $fig->get_release_info();      my $my_release = [$fig->get_release_info()];
1355      my $compatible = (defined($my_release) && ($my_release == $his_release)) ? 1 : 0;  
1356        #
1357        # Release id is $my_release->[1].
1358        #
1359    
1360        my $compatible;
1361        if ($my_release->[1] ne "" and $his_release->[1] ne "")
1362        {
1363            #
1364            # Both releases must be defined for them to be compatible.
1365            #
1366            # At some point we need to consider the derived-release issue.
1367            #
1368    
1369            $compatible = $my_release->[1] eq $his_release->[1];
1370        }
1371        else
1372        {
1373            $compatible = 0;
1374        }
1375    
1376      open(my $fh, ">$spool_dir/INFO");      open(my $fh, ">$spool_dir/INFO");
1377      print $fh "requestor_release\t$his_release\n";      print $fh "requestor_release\t$his_release\n";
1378      print $fh "last_update\t$last_update\n";      print $fh "last_update\t$last_update\n";
1379        print $fh "update_thru\t$update_thru\n";
1380      print $fh "cur_update\t$now\n";      print $fh "cur_update\t$now\n";
1381      print $fh "target_release\t$my_release\n";      print $fh "target_release\t$my_release\n";
1382      print $fh "compatible\t$compatible\n";      print $fh "compatible\t$compatible\n";
1383      print $fh "num_pegs\t$num_pegs\n";      print $fh "num_pegs\t$num_pegs\n";
1384      print $fh "num_genomes\t$num_genomes\n";      print $fh "num_genomes\t$num_genomes\n";
1385      print $fh "num_annos\t$num_annos\n";      print $fh "num_annos\t$num_annos\n";
1386        print $fh "num_assignments\t$num_assignments\n";
1387      close($fh);      close($fh);
1388    
1389      return [$session_id, $my_release, $num_annos, $num_pegs, $num_genomes, $now, $compatible];      #
1390        # Construct list of users, and pdate local user database.
1391        #
1392    
1393        my @users = keys(%users);
1394        # $fig->ensure_users(\@users);
1395    
1396        return [$session_id, $my_release, $num_assignments, $num_annos, $num_pegs, $num_genomes,
1397                $now, $compatible, \@users];
1398  }  }
1399    
1400    
# Line 801  Line 1529 
1529              #              #
1530              # Return the location and contig checksum for this peg.              # Return the location and contig checksum for this peg.
1531              #              #
1532                # We also include the sequence in case the contig mapping doesn't work.
1533                #
1534    
1535              my $loc = $fig->feature_location($peg);              my $loc = $fig->feature_location($peg);
1536              my $contig = $fig->contig_of($loc);              my $contig = $fig->contig_of($loc);
1537              my $cksum = $fig->contig_checksum($fig->genome_of($peg), $contig);              my $cksum = $fig->contig_checksum($fig->genome_of($peg), $contig);
1538              warn "Checksum for '$loc' '$contig' is $cksum\n";              my $seq = $fig->get_translation($peg);
1539    
1540              push(@$out, ['peg_loc', $peg,              push(@$out, ['peg_loc', $peg,
1541                          $fig->strand_of($loc),                          $fig->strand_of($peg),
1542                          $fig->beg_of($loc), $fig->end_of($loc),                          $fig->beg_of($loc), $fig->end_of($loc),
1543                          $cksum]);                          $cksum, $seq]);
1544    
1545          }          }
1546          elsif ($what eq "peg_unknown")          elsif ($what eq "peg_unknown")
# Line 822  Line 1552 
1552      return $out;      return $out;
1553  }  }
1554    
1555    
1556    sub get_annotations
1557    {
1558        my($self, $session_id, $start, $len) = @_;
1559    
1560        #
1561        # This is now easy; just run thru the saved annotations and return.
1562        #
1563    
1564        my(%session_info);
1565    
1566        my $spool_dir = "$FIG_Config::temp/p2p_spool/$session_id";
1567    
1568        -d $spool_dir or die "Invalid session id $session_id";
1569    
1570        #
1571        # Read in the cached information for this session.
1572        #
1573    
1574        open(my $info_fh, "<$spool_dir/INFO") or die "Cannot open INFO file: $!";
1575        while (<$info_fh>)
1576        {
1577            chomp;
1578            my($var, $val) = split(/\t/, $_, 2);
1579            $session_info{$var} = $val;
1580        }
1581        close($info_fh);
1582    
1583        #
1584        # Sanity check start and length.
1585        #
1586    
1587        if ($start < 0 or $start >= $session_info{num_annos})
1588        {
1589            die "Invalid start position $start";
1590        }
1591    
1592        if ($len < 0 or ($start + $len - 1) >= $session_info{num_annos})
1593        {
1594            die "Invalid length $len";
1595        }
1596    
1597        #
1598        # Open file, spin to the starting line, then start reading.
1599        #
1600    
1601        open(my $anno_fh, "<$spool_dir/annos") or die "Cannot open annos file: $!";
1602    
1603        my $anno_output = [];
1604    
1605        my $anno_num = 0;
1606    
1607        local $/ = "//\n";
1608        while (<$anno_fh>)
1609        {
1610            next if ($anno_num < $start);
1611    
1612            last if ($anno_num > ($start + $len));
1613    
1614            chomp;
1615    
1616            my($id, $date, $author, $anno) = split(/\n/, $_, 4);
1617    
1618            push(@$anno_output, [$id, $date, $author, $anno]);
1619        }
1620        continue
1621        {
1622            $anno_num++;
1623        }
1624    
1625        return $anno_output;
1626    }
1627    
1628    sub get_assignments
1629    {
1630        my($self, $session_id, $start, $len) = @_;
1631    
1632        #
1633        # This is now easy; just run thru the saved assignments and return.
1634        #
1635    
1636        my(%session_info);
1637    
1638        my $spool_dir = "$FIG_Config::temp/p2p_spool/$session_id";
1639    
1640        -d $spool_dir or die "Invalid session id $session_id";
1641    
1642        #
1643        # Read in the cached information for this session.
1644        #
1645    
1646        open(my $info_fh, "<$spool_dir/INFO") or die "Cannot open INFO file: $!";
1647        while (<$info_fh>)
1648        {
1649            chomp;
1650            my($var, $val) = split(/\t/, $_, 2);
1651            $session_info{$var} = $val;
1652        }
1653        close($info_fh);
1654    
1655        #
1656        # Sanity check start and length.
1657        #
1658    
1659        if ($start < 0 or $start >= $session_info{num_assignments})
1660        {
1661            die "Invalid start position $start";
1662        }
1663    
1664        if ($len < 0 or ($start + $len - 1) >= $session_info{num_assignments})
1665        {
1666            die "Invalid length $len";
1667        }
1668    
1669        #
1670        # Open file, spin to the starting line, then start reading.
1671        #
1672    
1673        open(my $assign_fh, "<$spool_dir/assignments") or die "Cannot open assignments file: $!";
1674    
1675        my $assign_output = [];
1676    
1677        my $assign_num = 0;
1678    
1679        while (<$assign_fh>)
1680        {
1681            next if ($assign_num < $start);
1682    
1683            last if ($assign_num > ($start + $len));
1684    
1685            chomp;
1686    
1687            my($id, $date, $author, $func) = split(/\t/, $_, 4);
1688    
1689            push(@$assign_output, [$id, $date, $author, $func]);
1690        }
1691        continue
1692        {
1693            $assign_num++;
1694        }
1695    
1696        return $assign_output;
1697    }
1698    
1699    1;

Legend:
Removed from v.1.12  
changed lines
  Added in v.1.25

MCS Webmaster
ViewVC Help
Powered by ViewVC 1.0.3