/[Search-Estraier]/trunk/lib/Search/Estraier.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Annotation of /trunk/lib/Search/Estraier.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 78 - (hide annotations)
Mon Jan 16 21:42:09 2006 UTC (18 years, 3 months ago) by dpavlin
Original Path: trunk/Estraier.pm
File size: 29371 byte(s)
added croak_on_error
1 dpavlin 2 package Search::Estraier;
2    
3     use 5.008;
4     use strict;
5     use warnings;
6    
7 dpavlin 76 our $VERSION = '0.04_1';
8 dpavlin 2
9     =head1 NAME
10    
11     Search::Estraier - pure perl module to use Hyper Estraier search engine
12    
13     =head1 SYNOPSIS
14    
15 dpavlin 68 =head2 Simple indexer
16 dpavlin 2
17 dpavlin 68 use Search::Estraier;
18    
19     # create and configure node
20     my $node = new Search::Estraier::Node;
21     $node->set_url("http://localhost:1978/node/test");
22     $node->set_auth("admin","admin");
23    
24     # create document
25     my $doc = new Search::Estraier::Document;
26    
27     # add attributes
28     $doc->add_attr('@uri', "http://estraier.gov/example.txt");
29     $doc->add_attr('@title', "Over the Rainbow");
30    
31     # add body text to document
32     $doc->add_text("Somewhere over the rainbow. Way up high.");
33     $doc->add_text("There's a land that I heard of once in a lullaby.");
34    
35     die "error: ", $node->status,"\n" unless ($node->put_doc($doc));
36    
37     =head2 Simple searcher
38    
39     use Search::Estraier;
40    
41     # create and configure node
42     my $node = new Search::Estraier::Node;
43     $node->set_url("http://localhost:1978/node/test");
44     $node->set_auth("admin","admin");
45    
46     # create condition
47     my $cond = new Search::Estraier::Condition;
48    
49     # set search phrase
50     $cond->set_phrase("rainbow AND lullaby");
51    
52     my $nres = $node->search($cond, 0);
53     if (defined($nres)) {
54     # for each document in results
55     for my $i ( 0 ... $nres->doc_num - 1 ) {
56     # get result document
57     my $rdoc = $nres->get_doc($i);
58     # display attribte
59     print "URI: ", $rdoc->attr('@uri'),"\n";
60     print "Title: ", $rdoc->attr('@title'),"\n";
61     print $rdoc->snippet,"\n";
62     }
63     } else {
64     die "error: ", $node->status,"\n";
65     }
66    
67 dpavlin 2 =head1 DESCRIPTION
68    
69     This module is implementation of node API of Hyper Estraier. Since it's
70     perl-only module with dependencies only on standard perl modules, it will
71     run on all platforms on which perl runs. It doesn't require compilation
72     or Hyper Estraier development files on target machine.
73    
74     It is implemented as multiple packages which closly resamble Ruby
75     implementation. It also includes methods to manage nodes.
76    
77 dpavlin 68 There are few examples in C<scripts> directory of this distribution.
78    
79 dpavlin 2 =cut
80    
81 dpavlin 42 =head1 Inheritable common methods
82    
83     This methods should really move somewhere else.
84    
85 dpavlin 15 =head2 _s
86    
87     Remove multiple whitespaces from string, as well as whitespaces at beginning or end
88    
89     my $text = $self->_s(" this is a text ");
90     $text = 'this is a text';
91    
92     =cut
93    
94     sub _s {
95     my $text = $_[1] || return;
96     $text =~ s/\s\s+/ /gs;
97     $text =~ s/^\s+//;
98     $text =~ s/\s+$//;
99     return $text;
100     }
101    
102 dpavlin 2 package Search::Estraier::Document;
103    
104 dpavlin 9 use Carp qw/croak confess/;
105 dpavlin 7
106 dpavlin 15 use Search::Estraier;
107     our @ISA = qw/Search::Estraier/;
108    
109 dpavlin 2 =head1 Search::Estraier::Document
110    
111 dpavlin 14 This class implements Document which is collection of attributes
112     (key=value), vectors (also key value) display text and hidden text.
113    
114 dpavlin 42
115 dpavlin 2 =head2 new
116    
117 dpavlin 14 Create new document, empty or from draft.
118    
119 dpavlin 2 my $doc = new Search::HyperEstraier::Document;
120 dpavlin 14 my $doc2 = new Search::HyperEstraier::Document( $draft );
121 dpavlin 2
122     =cut
123    
124     sub new {
125     my $class = shift;
126 dpavlin 14 my $self = {};
127 dpavlin 2 bless($self, $class);
128    
129 dpavlin 6 $self->{id} = -1;
130    
131 dpavlin 14 my $draft = shift;
132    
133     if ($draft) {
134     my $in_text = 0;
135     foreach my $line (split(/\n/, $draft)) {
136    
137     if ($in_text) {
138     if ($line =~ /^\t/) {
139     push @{ $self->{htexts} }, substr($line, 1);
140     } else {
141     push @{ $self->{dtexts} }, $line;
142     }
143     next;
144     }
145    
146     if ($line =~ m/^%VECTOR\t(.+)$/) {
147     my @fields = split(/\t/, $1);
148     for my $i ( 0 .. ($#fields - 1) ) {
149     $self->{kwords}->{ $fields[ $i ] } = $fields[ $i + 1 ];
150     $i++;
151     }
152     next;
153     } elsif ($line =~ m/^%/) {
154     # What is this? comment?
155     #warn "$line\n";
156     next;
157     } elsif ($line =~ m/^$/) {
158     $in_text = 1;
159     next;
160     } elsif ($line =~ m/^(.+)=(.+)$/) {
161     $self->{attrs}->{ $1 } = $2;
162     next;
163     }
164    
165     warn "draft ignored: $line\n";
166     }
167     }
168    
169 dpavlin 2 $self ? return $self : return undef;
170     }
171    
172 dpavlin 4
173 dpavlin 2 =head2 add_attr
174    
175 dpavlin 6 Add an attribute.
176    
177 dpavlin 2 $doc->add_attr( name => 'value' );
178    
179 dpavlin 9 Delete attribute using
180 dpavlin 5
181     $doc->add_attr( name => undef );
182    
183 dpavlin 2 =cut
184    
185     sub add_attr {
186     my $self = shift;
187     my $attrs = {@_};
188    
189     while (my ($name, $value) = each %{ $attrs }) {
190 dpavlin 9 if (! defined($value)) {
191 dpavlin 15 delete( $self->{attrs}->{ $self->_s($name) } );
192 dpavlin 9 } else {
193 dpavlin 15 $self->{attrs}->{ $self->_s($name) } = $self->_s($value);
194 dpavlin 9 }
195 dpavlin 2 }
196 dpavlin 8
197     return 1;
198 dpavlin 2 }
199    
200 dpavlin 5
201     =head2 add_text
202    
203 dpavlin 6 Add a sentence of text.
204    
205 dpavlin 5 $doc->add_text('this is example text to display');
206    
207     =cut
208    
209     sub add_text {
210     my $self = shift;
211     my $text = shift;
212     return unless defined($text);
213    
214 dpavlin 15 push @{ $self->{dtexts} }, $self->_s($text);
215 dpavlin 5 }
216    
217    
218     =head2 add_hidden_text
219    
220 dpavlin 6 Add a hidden sentence.
221    
222 dpavlin 5 $doc->add_hidden_text('this is example text just for search');
223    
224     =cut
225    
226     sub add_hidden_text {
227     my $self = shift;
228     my $text = shift;
229     return unless defined($text);
230    
231 dpavlin 15 push @{ $self->{htexts} }, $self->_s($text);
232 dpavlin 5 }
233    
234 dpavlin 42
235 dpavlin 6 =head2 id
236    
237     Get the ID number of document. If the object has never been registred, C<-1> is returned.
238    
239     print $doc->id;
240    
241     =cut
242    
243     sub id {
244     my $self = shift;
245     return $self->{id};
246     }
247    
248 dpavlin 42
249 dpavlin 7 =head2 attr_names
250    
251 dpavlin 9 Returns array with attribute names from document object.
252 dpavlin 7
253     my @attrs = $doc->attr_names;
254    
255     =cut
256    
257     sub attr_names {
258     my $self = shift;
259 dpavlin 60 return unless ($self->{attrs});
260     #croak "attr_names return array, not scalar" if (! wantarray);
261 dpavlin 7 return sort keys %{ $self->{attrs} };
262     }
263    
264 dpavlin 8
265     =head2 attr
266    
267 dpavlin 9 Returns value of an attribute.
268 dpavlin 8
269     my $value = $doc->attr( 'attribute' );
270    
271     =cut
272    
273     sub attr {
274     my $self = shift;
275     my $name = shift;
276 dpavlin 60 return unless (defined($name) && $self->{attrs});
277     return $self->{attrs}->{ $name };
278 dpavlin 8 }
279    
280 dpavlin 9
281     =head2 texts
282    
283     Returns array with text sentences.
284    
285     my @texts = $doc->texts;
286    
287     =cut
288    
289     sub texts {
290     my $self = shift;
291 dpavlin 60 #confess "texts return array, not scalar" if (! wantarray);
292     return @{ $self->{dtexts} } if ($self->{dtexts});
293 dpavlin 9 }
294    
295 dpavlin 42
296 dpavlin 12 =head2 cat_texts
297    
298     Return whole text as single scalar.
299    
300     my $text = $doc->cat_texts;
301    
302     =cut
303    
304     sub cat_texts {
305     my $self = shift;
306 dpavlin 60 return join(' ',@{ $self->{dtexts} }) if ($self->{dtexts});
307 dpavlin 12 }
308    
309 dpavlin 42
310 dpavlin 5 =head2 dump_draft
311    
312 dpavlin 13 Dump draft data from document object.
313    
314 dpavlin 5 print $doc->dump_draft;
315    
316     =cut
317    
318     sub dump_draft {
319 dpavlin 13 my $self = shift;
320     my $draft;
321    
322     foreach my $attr_name (sort keys %{ $self->{attrs} }) {
323     $draft .= $attr_name . '=' . $self->{attrs}->{$attr_name} . "\n";
324     }
325    
326     if ($self->{kwords}) {
327     $draft .= '%%VECTOR';
328     while (my ($key, $value) = each %{ $self->{kwords} }) {
329     $draft .= "\t$key\t$value";
330     }
331     $draft .= "\n";
332     }
333    
334     $draft .= "\n";
335    
336 dpavlin 40 $draft .= join("\n", @{ $self->{dtexts} }) . "\n" if ($self->{dtexts});
337     $draft .= "\t" . join("\n\t", @{ $self->{htexts} }) . "\n" if ($self->{htexts});
338 dpavlin 13
339     return $draft;
340 dpavlin 5 }
341    
342 dpavlin 42
343 dpavlin 4 =head2 delete
344 dpavlin 2
345 dpavlin 4 Empty document object
346 dpavlin 2
347 dpavlin 4 $doc->delete;
348    
349 dpavlin 15 This function is addition to original Ruby API, and since it was included in C wrappers it's here as a
350     convinience. Document objects which go out of scope will be destroyed
351     automatically.
352    
353 dpavlin 4 =cut
354    
355     sub delete {
356     my $self = shift;
357    
358 dpavlin 14 foreach my $data (qw/attrs dtexts stexts kwords/) {
359 dpavlin 5 delete($self->{$data});
360     }
361 dpavlin 4
362 dpavlin 10 $self->{id} = -1;
363    
364 dpavlin 4 return 1;
365     }
366    
367    
368    
369 dpavlin 15 package Search::Estraier::Condition;
370 dpavlin 4
371 dpavlin 16 use Carp qw/confess croak/;
372    
373 dpavlin 15 use Search::Estraier;
374     our @ISA = qw/Search::Estraier/;
375 dpavlin 4
376 dpavlin 16 =head1 Search::Estraier::Condition
377    
378     =head2 new
379    
380     my $cond = new Search::HyperEstraier::Condition;
381    
382     =cut
383    
384     sub new {
385     my $class = shift;
386     my $self = {};
387     bless($self, $class);
388    
389 dpavlin 19 $self->{max} = -1;
390     $self->{options} = 0;
391    
392 dpavlin 16 $self ? return $self : return undef;
393     }
394    
395 dpavlin 42
396 dpavlin 16 =head2 set_phrase
397    
398     $cond->set_phrase('search phrase');
399    
400     =cut
401    
402     sub set_phrase {
403     my $self = shift;
404     $self->{phrase} = $self->_s( shift );
405     }
406    
407 dpavlin 42
408 dpavlin 16 =head2 add_attr
409    
410     $cond->add_attr('@URI STRINC /~dpavlin/');
411    
412     =cut
413    
414     sub add_attr {
415     my $self = shift;
416     my $attr = shift || return;
417     push @{ $self->{attrs} }, $self->_s( $attr );
418     }
419    
420 dpavlin 42
421 dpavlin 16 =head2 set_order
422    
423     $cond->set_order('@mdate NUMD');
424    
425     =cut
426    
427     sub set_order {
428     my $self = shift;
429     $self->{order} = shift;
430     }
431    
432 dpavlin 42
433 dpavlin 16 =head2 set_max
434    
435     $cond->set_max(42);
436    
437     =cut
438    
439     sub set_max {
440     my $self = shift;
441     my $max = shift;
442 dpavlin 43 croak "set_max needs number, not '$max'" unless ($max =~ m/^\d+$/);
443 dpavlin 16 $self->{max} = $max;
444     }
445    
446 dpavlin 42
447 dpavlin 16 =head2 set_options
448    
449     $cond->set_options( SURE => 1 );
450    
451     =cut
452    
453 dpavlin 15 my $options = {
454     # check N-gram keys skipping by three
455     SURE => 1 << 0,
456     # check N-gram keys skipping by two
457     USUAL => 1 << 1,
458     # without TF-IDF tuning
459     FAST => 1 << 2,
460     # with the simplified phrase
461     AGITO => 1 << 3,
462     # check every N-gram key
463     NOIDF => 1 << 4,
464     # check N-gram keys skipping by one
465     SIMPLE => 1 << 10,
466     };
467    
468 dpavlin 16 sub set_options {
469     my $self = shift;
470     my $option = shift;
471     confess "unknown option" unless ($options->{$option});
472     $self->{options} ||= $options->{$option};
473 dpavlin 4 }
474    
475 dpavlin 42
476 dpavlin 18 =head2 phrase
477    
478     Return search phrase.
479    
480     print $cond->phrase;
481    
482     =cut
483    
484     sub phrase {
485     my $self = shift;
486     return $self->{phrase};
487     }
488    
489 dpavlin 42
490 dpavlin 19 =head2 order
491 dpavlin 18
492 dpavlin 19 Return search result order.
493    
494     print $cond->order;
495    
496     =cut
497    
498     sub order {
499     my $self = shift;
500     return $self->{order};
501     }
502    
503 dpavlin 42
504 dpavlin 19 =head2 attrs
505    
506     Return search result attrs.
507    
508     my @cond_attrs = $cond->attrs;
509    
510     =cut
511    
512     sub attrs {
513     my $self = shift;
514     #croak "attrs return array, not scalar" if (! wantarray);
515 dpavlin 60 return @{ $self->{attrs} } if ($self->{attrs});
516 dpavlin 19 }
517    
518 dpavlin 42
519 dpavlin 19 =head2 max
520    
521     Return maximum number of results.
522    
523     print $cond->max;
524    
525     C<-1> is returned for unitialized value, C<0> is unlimited.
526    
527     =cut
528    
529     sub max {
530     my $self = shift;
531     return $self->{max};
532     }
533    
534 dpavlin 42
535 dpavlin 19 =head2 options
536    
537     Return options for this condition.
538    
539     print $cond->options;
540    
541     Options are returned in numerical form.
542    
543     =cut
544    
545     sub options {
546     my $self = shift;
547     return $self->{options};
548     }
549    
550    
551 dpavlin 20 package Search::Estraier::ResultDocument;
552    
553 dpavlin 24 use Carp qw/croak/;
554 dpavlin 20
555 dpavlin 24 #use Search::Estraier;
556     #our @ISA = qw/Search::Estraier/;
557 dpavlin 20
558     =head1 Search::Estraier::ResultDocument
559    
560     =head2 new
561    
562 dpavlin 23 my $rdoc = new Search::HyperEstraier::ResultDocument(
563 dpavlin 20 uri => 'http://localhost/document/uri/42',
564     attrs => {
565     foo => 1,
566     bar => 2,
567     },
568     snippet => 'this is a text of snippet'
569     keywords => 'this\tare\tkeywords'
570     );
571    
572     =cut
573    
574     sub new {
575     my $class = shift;
576     my $self = {@_};
577     bless($self, $class);
578    
579 dpavlin 62 croak "missing uri for ResultDocument" unless defined($self->{uri});
580 dpavlin 20
581     $self ? return $self : return undef;
582     }
583    
584 dpavlin 42
585 dpavlin 23 =head2 uri
586 dpavlin 20
587 dpavlin 23 Return URI of result document
588 dpavlin 20
589 dpavlin 23 print $rdoc->uri;
590    
591     =cut
592    
593     sub uri {
594     my $self = shift;
595     return $self->{uri};
596     }
597    
598    
599     =head2 attr_names
600    
601     Returns array with attribute names from result document object.
602    
603     my @attrs = $rdoc->attr_names;
604    
605     =cut
606    
607     sub attr_names {
608     my $self = shift;
609     croak "attr_names return array, not scalar" if (! wantarray);
610     return sort keys %{ $self->{attrs} };
611     }
612    
613 dpavlin 42
614 dpavlin 23 =head2 attr
615    
616     Returns value of an attribute.
617    
618     my $value = $rdoc->attr( 'attribute' );
619    
620     =cut
621    
622     sub attr {
623     my $self = shift;
624     my $name = shift || return;
625     return $self->{attrs}->{ $name };
626     }
627    
628 dpavlin 42
629 dpavlin 23 =head2 snippet
630    
631     Return snippet from result document
632    
633     print $rdoc->snippet;
634    
635     =cut
636    
637     sub snippet {
638     my $self = shift;
639     return $self->{snippet};
640     }
641    
642 dpavlin 42
643 dpavlin 23 =head2 keywords
644    
645     Return keywords from result document
646    
647     print $rdoc->keywords;
648    
649     =cut
650    
651     sub keywords {
652     my $self = shift;
653     return $self->{keywords};
654     }
655    
656    
657 dpavlin 25 package Search::Estraier::NodeResult;
658    
659     use Carp qw/croak/;
660    
661     #use Search::Estraier;
662     #our @ISA = qw/Search::Estraier/;
663    
664     =head1 Search::Estraier::NodeResult
665    
666     =head2 new
667    
668     my $res = new Search::HyperEstraier::NodeResult(
669     docs => @array_of_rdocs,
670     hits => %hash_with_hints,
671     );
672    
673     =cut
674    
675     sub new {
676     my $class = shift;
677     my $self = {@_};
678     bless($self, $class);
679    
680     foreach my $f (qw/docs hints/) {
681     croak "missing $f for ResultDocument" unless defined($self->{$f});
682     }
683    
684     $self ? return $self : return undef;
685     }
686    
687 dpavlin 42
688 dpavlin 25 =head2 doc_num
689    
690     Return number of documents
691    
692     print $res->doc_num;
693    
694     =cut
695    
696     sub doc_num {
697     my $self = shift;
698 dpavlin 53 return $#{$self->{docs}} + 1;
699 dpavlin 25 }
700    
701 dpavlin 42
702 dpavlin 25 =head2 get_doc
703    
704     Return single document
705    
706     my $doc = $res->get_doc( 42 );
707    
708     Returns undef if document doesn't exist.
709    
710     =cut
711    
712     sub get_doc {
713     my $self = shift;
714     my $num = shift;
715 dpavlin 43 croak "expect number as argument, not '$num'" unless ($num =~ m/^\d+$/);
716 dpavlin 25 return undef if ($num < 0 || $num > $self->{docs});
717     return $self->{docs}->[$num];
718     }
719    
720 dpavlin 42
721 dpavlin 25 =head2 hint
722    
723     Return specific hint from results.
724    
725     print $rec->hint( 'VERSION' );
726    
727     Possible hints are: C<VERSION>, C<NODE>, C<HIT>, C<HINT#n>, C<DOCNUM>, C<WORDNUM>,
728     C<TIME>, C<LINK#n>, C<VIEW>.
729    
730     =cut
731    
732     sub hint {
733     my $self = shift;
734     my $key = shift || return;
735     return $self->{hints}->{$key};
736     }
737    
738    
739 dpavlin 27 package Search::Estraier::Node;
740    
741 dpavlin 44 use Carp qw/carp croak confess/;
742 dpavlin 33 use URI;
743 dpavlin 36 use MIME::Base64;
744 dpavlin 33 use IO::Socket::INET;
745 dpavlin 49 use URI::Escape qw/uri_escape/;
746 dpavlin 29
747 dpavlin 27 =head1 Search::Estraier::Node
748    
749     =head2 new
750    
751     my $node = new Search::HyperEstraier::Node;
752    
753 dpavlin 65 or optionally with C<url> as parametar
754    
755     my $node = new Search::HyperEstraier::Node( 'http://localhost:1978/node/test' );
756    
757 dpavlin 78 or in more verbose form
758    
759     my $node = new Search::HyperEstraier::Node(
760     url => 'http://localhost:1978/node/test',
761     debug => 1,
762     croak_on_error => 1
763     );
764    
765     with following arguments:
766    
767     =over 4
768    
769     =item url
770    
771     URL to node
772    
773     =item debug
774    
775     dumps a B<lot> of debugging output
776    
777     =item croak_on_error
778    
779     very helpful during development. It will croak on all errors instead of
780     silently returning C<-1> (which is convention of Hyper Estraier API in other
781     languages).
782    
783     =back
784    
785 dpavlin 27 =cut
786    
787     sub new {
788     my $class = shift;
789     my $self = {
790     pxport => -1,
791 dpavlin 33 timeout => 0, # this used to be -1
792 dpavlin 27 dnum => -1,
793     wnum => -1,
794     size => -1.0,
795     wwidth => 480,
796     hwidth => 96,
797     awidth => 96,
798     status => -1,
799     };
800     bless($self, $class);
801    
802 dpavlin 65 if ($#_ == 0) {
803     $self->{url} = shift;
804     } else {
805     my $args = {@_};
806 dpavlin 39
807 dpavlin 78 %$self = ( %$self, @_ );
808    
809 dpavlin 65 warn "## Node debug on\n" if ($self->{debug});
810     }
811 dpavlin 57
812 dpavlin 27 $self ? return $self : return undef;
813     }
814    
815 dpavlin 42
816 dpavlin 29 =head2 set_url
817    
818     Specify URL to node server
819    
820     $node->set_url('http://localhost:1978');
821    
822     =cut
823    
824     sub set_url {
825     my $self = shift;
826     $self->{url} = shift;
827     }
828    
829 dpavlin 42
830 dpavlin 29 =head2 set_proxy
831    
832     Specify proxy server to connect to node server
833    
834     $node->set_proxy('proxy.example.com', 8080);
835    
836     =cut
837    
838     sub set_proxy {
839     my $self = shift;
840     my ($host,$port) = @_;
841 dpavlin 43 croak "proxy port must be number, not '$port'" unless ($port =~ m/^\d+$/);
842 dpavlin 29 $self->{pxhost} = $host;
843     $self->{pxport} = $port;
844     }
845    
846 dpavlin 42
847 dpavlin 30 =head2 set_timeout
848    
849     Specify timeout of connection in seconds
850    
851     $node->set_timeout( 15 );
852    
853     =cut
854    
855     sub set_timeout {
856     my $self = shift;
857     my $sec = shift;
858 dpavlin 43 croak "timeout must be number, not '$sec'" unless ($sec =~ m/^\d+$/);
859 dpavlin 30 $self->{timeout} = $sec;
860     }
861    
862 dpavlin 42
863 dpavlin 31 =head2 set_auth
864    
865     Specify name and password for authentication to node server.
866    
867     $node->set_auth('clint','eastwood');
868    
869     =cut
870    
871     sub set_auth {
872     my $self = shift;
873     my ($login,$passwd) = @_;
874 dpavlin 40 my $basic_auth = encode_base64( "$login:$passwd" );
875     chomp($basic_auth);
876     $self->{auth} = $basic_auth;
877 dpavlin 31 }
878    
879 dpavlin 42
880 dpavlin 32 =head2 status
881    
882     Return status code of last request.
883    
884 dpavlin 40 print $node->status;
885 dpavlin 32
886     C<-1> means connection failure.
887    
888     =cut
889    
890     sub status {
891     my $self = shift;
892     return $self->{status};
893     }
894    
895 dpavlin 42
896 dpavlin 40 =head2 put_doc
897    
898 dpavlin 41 Add a document
899 dpavlin 40
900 dpavlin 41 $node->put_doc( $document_draft ) or die "can't add document";
901    
902     Return true on success or false on failture.
903    
904 dpavlin 40 =cut
905    
906     sub put_doc {
907     my $self = shift;
908     my $doc = shift || return;
909 dpavlin 47 return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
910 dpavlin 41 $self->shuttle_url( $self->{url} . '/put_doc',
911     'text/x-estraier-draft',
912     $doc->dump_draft,
913     undef
914     ) == 200;
915 dpavlin 40 }
916    
917 dpavlin 41
918     =head2 out_doc
919    
920     Remove a document
921    
922     $node->out_doc( document_id ) or "can't remove document";
923    
924     Return true on success or false on failture.
925    
926     =cut
927    
928     sub out_doc {
929     my $self = shift;
930     my $id = shift || return;
931     return unless ($self->{url});
932 dpavlin 43 croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);
933 dpavlin 41 $self->shuttle_url( $self->{url} . '/out_doc',
934     'application/x-www-form-urlencoded',
935     "id=$id",
936     undef
937     ) == 200;
938     }
939    
940    
941     =head2 out_doc_by_uri
942    
943     Remove a registrated document using it's uri
944    
945 dpavlin 45 $node->out_doc_by_uri( 'file:///document/uri/42' ) or "can't remove document";
946 dpavlin 41
947     Return true on success or false on failture.
948    
949     =cut
950    
951     sub out_doc_by_uri {
952     my $self = shift;
953     my $uri = shift || return;
954     return unless ($self->{url});
955     $self->shuttle_url( $self->{url} . '/out_doc',
956     'application/x-www-form-urlencoded',
957 dpavlin 50 "uri=" . uri_escape($uri),
958 dpavlin 41 undef
959     ) == 200;
960     }
961    
962 dpavlin 42
963     =head2 edit_doc
964    
965     Edit attributes of a document
966    
967     $node->edit_doc( $document_draft ) or die "can't edit document";
968    
969     Return true on success or false on failture.
970    
971     =cut
972    
973     sub edit_doc {
974     my $self = shift;
975     my $doc = shift || return;
976 dpavlin 47 return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
977 dpavlin 42 $self->shuttle_url( $self->{url} . '/edit_doc',
978     'text/x-estraier-draft',
979     $doc->dump_draft,
980     undef
981     ) == 200;
982     }
983    
984    
985 dpavlin 43 =head2 get_doc
986    
987     Retreive document
988    
989     my $doc = $node->get_doc( document_id ) or die "can't get document";
990    
991     Return true on success or false on failture.
992    
993     =cut
994    
995     sub get_doc {
996     my $self = shift;
997     my $id = shift || return;
998     return $self->_fetch_doc( id => $id );
999     }
1000    
1001 dpavlin 44
1002 dpavlin 43 =head2 get_doc_by_uri
1003    
1004     Retreive document
1005    
1006 dpavlin 45 my $doc = $node->get_doc_by_uri( 'file:///document/uri/42' ) or die "can't get document";
1007 dpavlin 43
1008     Return true on success or false on failture.
1009    
1010     =cut
1011    
1012     sub get_doc_by_uri {
1013     my $self = shift;
1014     my $uri = shift || return;
1015     return $self->_fetch_doc( uri => $uri );
1016     }
1017    
1018 dpavlin 44
1019 dpavlin 49 =head2 get_doc_attr
1020    
1021     Retrieve the value of an atribute from object
1022    
1023     my $val = $node->get_doc_attr( document_id, 'attribute_name' ) or
1024     die "can't get document attribute";
1025    
1026     =cut
1027    
1028     sub get_doc_attr {
1029     my $self = shift;
1030     my ($id,$name) = @_;
1031     return unless ($id && $name);
1032     return $self->_fetch_doc( id => $id, attr => $name );
1033     }
1034    
1035    
1036     =head2 get_doc_attr_by_uri
1037    
1038     Retrieve the value of an atribute from object
1039    
1040     my $val = $node->get_doc_attr_by_uri( document_id, 'attribute_name' ) or
1041     die "can't get document attribute";
1042    
1043     =cut
1044    
1045     sub get_doc_attr_by_uri {
1046     my $self = shift;
1047     my ($uri,$name) = @_;
1048     return unless ($uri && $name);
1049     return $self->_fetch_doc( uri => $uri, attr => $name );
1050     }
1051    
1052    
1053 dpavlin 44 =head2 etch_doc
1054    
1055     Exctract document keywords
1056    
1057     my $keywords = $node->etch_doc( document_id ) or die "can't etch document";
1058    
1059     =cut
1060    
1061 dpavlin 49 sub etch_doc {
1062 dpavlin 44 my $self = shift;
1063     my $id = shift || return;
1064     return $self->_fetch_doc( id => $id, etch => 1 );
1065     }
1066    
1067     =head2 etch_doc_by_uri
1068    
1069     Retreive document
1070    
1071 dpavlin 45 my $keywords = $node->etch_doc_by_uri( 'file:///document/uri/42' ) or die "can't etch document";
1072 dpavlin 44
1073     Return true on success or false on failture.
1074    
1075     =cut
1076    
1077     sub etch_doc_by_uri {
1078     my $self = shift;
1079     my $uri = shift || return;
1080     return $self->_fetch_doc( uri => $uri, etch => 1 );
1081     }
1082    
1083    
1084 dpavlin 45 =head2 uri_to_id
1085    
1086     Get ID of document specified by URI
1087    
1088     my $id = $node->uri_to_id( 'file:///document/uri/42' );
1089    
1090     =cut
1091    
1092     sub uri_to_id {
1093     my $self = shift;
1094     my $uri = shift || return;
1095     return $self->_fetch_doc( uri => $uri, path => '/uri_to_id', chomp_resbody => 1 );
1096     }
1097    
1098    
1099 dpavlin 43 =head2 _fetch_doc
1100    
1101 dpavlin 44 Private function used for implementing of C<get_doc>, C<get_doc_by_uri>,
1102     C<etch_doc>, C<etch_doc_by_uri>.
1103 dpavlin 43
1104 dpavlin 45 # this will decode received draft into Search::Estraier::Document object
1105     my $doc = $node->_fetch_doc( id => 42 );
1106     my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42' );
1107 dpavlin 43
1108 dpavlin 45 # to extract keywords, add etch
1109     my $doc = $node->_fetch_doc( id => 42, etch => 1 );
1110     my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42', etch => 1 );
1111    
1112 dpavlin 49 # to get document attrubute add attr
1113     my $doc = $node->_fetch_doc( id => 42, attr => '@mdate' );
1114     my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42', attr => '@mdate' );
1115    
1116 dpavlin 45 # more general form which allows implementation of
1117     # uri_to_id
1118     my $id = $node->_fetch_doc(
1119     uri => 'file:///document/uri/42',
1120     path => '/uri_to_id',
1121     chomp_resbody => 1
1122     );
1123    
1124 dpavlin 43 =cut
1125    
1126     sub _fetch_doc {
1127     my $self = shift;
1128 dpavlin 44 my $a = {@_};
1129     return unless ( ($a->{id} || $a->{uri}) && $self->{url} );
1130    
1131     my ($arg, $resbody);
1132    
1133 dpavlin 45 my $path = $a->{path} || '/get_doc';
1134 dpavlin 44 $path = '/etch_doc' if ($a->{etch});
1135    
1136     if ($a->{id}) {
1137     croak "id must be numberm not '$a->{id}'" unless ($a->{id} =~ m/^\d+$/);
1138     $arg = 'id=' . $a->{id};
1139     } elsif ($a->{uri}) {
1140 dpavlin 50 $arg = 'uri=' . uri_escape($a->{uri});
1141 dpavlin 44 } else {
1142     confess "unhandled argument. Need id or uri.";
1143 dpavlin 43 }
1144 dpavlin 44
1145 dpavlin 49 if ($a->{attr}) {
1146     $path = '/get_doc_attr';
1147     $arg .= '&attr=' . uri_escape($a->{attr});
1148     $a->{chomp_resbody} = 1;
1149     }
1150    
1151 dpavlin 44 my $rv = $self->shuttle_url( $self->{url} . $path,
1152 dpavlin 43 'application/x-www-form-urlencoded',
1153 dpavlin 44 $arg,
1154 dpavlin 45 \$resbody,
1155 dpavlin 43 );
1156 dpavlin 44
1157 dpavlin 43 return if ($rv != 200);
1158 dpavlin 44
1159     if ($a->{etch}) {
1160     $self->{kwords} = {};
1161     return +{} unless ($resbody);
1162     foreach my $l (split(/\n/, $resbody)) {
1163     my ($k,$v) = split(/\t/, $l, 2);
1164     $self->{kwords}->{$k} = $v if ($v);
1165     }
1166     return $self->{kwords};
1167 dpavlin 45 } elsif ($a->{chomp_resbody}) {
1168     return unless (defined($resbody));
1169     chomp($resbody);
1170     return $resbody;
1171 dpavlin 44 } else {
1172     return new Search::Estraier::Document($resbody);
1173     }
1174 dpavlin 43 }
1175    
1176    
1177 dpavlin 48 =head2 name
1178 dpavlin 43
1179 dpavlin 48 my $node_name = $node->name;
1180 dpavlin 43
1181 dpavlin 48 =cut
1182    
1183     sub name {
1184     my $self = shift;
1185 dpavlin 55 $self->_set_info unless ($self->{name});
1186 dpavlin 48 return $self->{name};
1187     }
1188    
1189    
1190     =head2 label
1191    
1192     my $node_label = $node->label;
1193    
1194     =cut
1195    
1196     sub label {
1197     my $self = shift;
1198 dpavlin 55 $self->_set_info unless ($self->{label});
1199 dpavlin 48 return $self->{label};
1200     }
1201    
1202    
1203     =head2 doc_num
1204    
1205     my $documents_in_node = $node->doc_num;
1206    
1207     =cut
1208    
1209     sub doc_num {
1210     my $self = shift;
1211 dpavlin 55 $self->_set_info if ($self->{dnum} < 0);
1212 dpavlin 48 return $self->{dnum};
1213     }
1214    
1215    
1216     =head2 word_num
1217    
1218     my $words_in_node = $node->word_num;
1219    
1220     =cut
1221    
1222     sub word_num {
1223     my $self = shift;
1224 dpavlin 55 $self->_set_info if ($self->{wnum} < 0);
1225 dpavlin 48 return $self->{wnum};
1226     }
1227    
1228    
1229     =head2 size
1230    
1231     my $node_size = $node->size;
1232    
1233     =cut
1234    
1235     sub size {
1236     my $self = shift;
1237 dpavlin 55 $self->_set_info if ($self->{size} < 0);
1238 dpavlin 48 return $self->{size};
1239     }
1240    
1241    
1242 dpavlin 51 =head2 search
1243 dpavlin 48
1244 dpavlin 51 Search documents which match condition
1245    
1246     my $nres = $node->search( $cond, $depth );
1247    
1248     C<$cond> is C<Search::Estraier::Condition> object, while <$depth> specifies
1249     depth for meta search.
1250    
1251     Function results C<Search::Estraier::NodeResult> object.
1252    
1253     =cut
1254    
1255     sub search {
1256     my $self = shift;
1257     my ($cond, $depth) = @_;
1258     return unless ($cond && defined($depth) && $self->{url});
1259     croak "cond mush be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));
1260     croak "depth needs number, not '$depth'" unless ($depth =~ m/^\d+$/);
1261    
1262 dpavlin 52 my $resbody;
1263 dpavlin 51
1264 dpavlin 52 my $rv = $self->shuttle_url( $self->{url} . '/search',
1265 dpavlin 53 'application/x-www-form-urlencoded',
1266 dpavlin 61 $self->cond_to_query( $cond, $depth ),
1267 dpavlin 52 \$resbody,
1268     );
1269     return if ($rv != 200);
1270    
1271     my (@docs, $hints);
1272    
1273     my @lines = split(/\n/, $resbody);
1274     return unless (@lines);
1275    
1276     my $border = $lines[0];
1277     my $isend = 0;
1278     my $lnum = 1;
1279    
1280     while ( $lnum <= $#lines ) {
1281     my $line = $lines[$lnum];
1282     $lnum++;
1283    
1284     #warn "## $line\n";
1285     if ($line && $line =~ m/^\Q$border\E(:END)*$/) {
1286     $isend = $1;
1287     last;
1288     }
1289    
1290     if ($line =~ /\t/) {
1291     my ($k,$v) = split(/\t/, $line, 2);
1292     $hints->{$k} = $v;
1293     }
1294     }
1295    
1296     my $snum = $lnum;
1297    
1298     while( ! $isend && $lnum <= $#lines ) {
1299     my $line = $lines[$lnum];
1300 dpavlin 53 #warn "# $lnum: $line\n";
1301 dpavlin 52 $lnum++;
1302    
1303     if ($line && $line =~ m/^\Q$border\E/) {
1304     if ($lnum > $snum) {
1305     my $rdattrs;
1306     my $rdvector;
1307     my $rdsnippet;
1308    
1309     my $rlnum = $snum;
1310     while ($rlnum < $lnum - 1 ) {
1311     #my $rdline = $self->_s($lines[$rlnum]);
1312     my $rdline = $lines[$rlnum];
1313     $rlnum++;
1314     last unless ($rdline);
1315     if ($rdline =~ /^%/) {
1316     $rdvector = $1 if ($rdline =~ /^%VECTOR\t(.+)$/);
1317 dpavlin 53 } elsif($rdline =~ /=/) {
1318     $rdattrs->{$1} = $2 if ($rdline =~ /^(.+)=(.+)$/);
1319 dpavlin 52 } else {
1320 dpavlin 53 confess "invalid format of response";
1321 dpavlin 52 }
1322     }
1323     while($rlnum < $lnum - 1) {
1324     my $rdline = $lines[$rlnum];
1325     $rlnum++;
1326     $rdsnippet .= "$rdline\n";
1327     }
1328 dpavlin 53 #warn Dumper($rdvector, $rdattrs, $rdsnippet);
1329 dpavlin 52 if (my $rduri = $rdattrs->{'@uri'}) {
1330     push @docs, new Search::Estraier::ResultDocument(
1331     uri => $rduri,
1332     attrs => $rdattrs,
1333     snippet => $rdsnippet,
1334     keywords => $rdvector,
1335     );
1336     }
1337     }
1338     $snum = $lnum;
1339     #warn "### $line\n";
1340     $isend = 1 if ($line =~ /:END$/);
1341     }
1342    
1343     }
1344    
1345     if (! $isend) {
1346     warn "received result doesn't have :END\n$resbody";
1347     return;
1348     }
1349    
1350 dpavlin 53 #warn Dumper(\@docs, $hints);
1351    
1352 dpavlin 52 return new Search::Estraier::NodeResult( docs => \@docs, hints => $hints );
1353 dpavlin 51 }
1354    
1355    
1356     =head2 cond_to_query
1357    
1358 dpavlin 55 Return URI encoded string generated from Search::Estraier::Condition
1359    
1360 dpavlin 61 my $args = $node->cond_to_query( $cond, $depth );
1361 dpavlin 51
1362     =cut
1363    
1364     sub cond_to_query {
1365     my $self = shift;
1366    
1367     my $cond = shift || return;
1368     croak "condition must be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));
1369 dpavlin 61 my $depth = shift;
1370 dpavlin 51
1371     my @args;
1372    
1373     if (my $phrase = $cond->phrase) {
1374     push @args, 'phrase=' . uri_escape($phrase);
1375     }
1376    
1377     if (my @attrs = $cond->attrs) {
1378     for my $i ( 0 .. $#attrs ) {
1379 dpavlin 63 push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] ) if ($attrs[$i]);
1380 dpavlin 51 }
1381     }
1382    
1383     if (my $order = $cond->order) {
1384     push @args, 'order=' . uri_escape($order);
1385     }
1386    
1387     if (my $max = $cond->max) {
1388     push @args, 'max=' . $max;
1389     } else {
1390     push @args, 'max=' . (1 << 30);
1391     }
1392    
1393     if (my $options = $cond->options) {
1394     push @args, 'options=' . $options;
1395     }
1396    
1397 dpavlin 61 push @args, 'depth=' . $depth if ($depth);
1398 dpavlin 51 push @args, 'wwidth=' . $self->{wwidth};
1399     push @args, 'hwidth=' . $self->{hwidth};
1400     push @args, 'awidth=' . $self->{awidth};
1401    
1402     return join('&', @args);
1403     }
1404    
1405    
1406 dpavlin 33 =head2 shuttle_url
1407 dpavlin 32
1408 dpavlin 68 This is method which uses C<LWP::UserAgent> to communicate with Hyper Estraier node
1409 dpavlin 33 master.
1410 dpavlin 2
1411 dpavlin 52 my $rv = shuttle_url( $url, $content_type, $req_body, \$resbody );
1412 dpavlin 2
1413 dpavlin 33 C<$resheads> and C<$resbody> booleans controll if response headers and/or response
1414     body will be saved within object.
1415 dpavlin 2
1416     =cut
1417    
1418 dpavlin 59 use LWP::UserAgent;
1419    
1420 dpavlin 33 sub shuttle_url {
1421     my $self = shift;
1422 dpavlin 2
1423 dpavlin 33 my ($url, $content_type, $reqbody, $resbody) = @_;
1424 dpavlin 2
1425 dpavlin 40 $self->{status} = -1;
1426 dpavlin 33
1427 dpavlin 41 warn "## $url\n" if ($self->{debug});
1428 dpavlin 36
1429 dpavlin 33 $url = new URI($url);
1430 dpavlin 37 if (
1431     !$url || !$url->scheme || !$url->scheme eq 'http' ||
1432     !$url->host || !$url->port || $url->port < 1
1433     ) {
1434     carp "can't parse $url\n";
1435     return -1;
1436     }
1437 dpavlin 33
1438 dpavlin 59 my $ua = LWP::UserAgent->new;
1439     $ua->agent( "Search-Estraier/$Search::Estraier::VERSION" );
1440 dpavlin 33
1441 dpavlin 59 my $req;
1442 dpavlin 37 if ($reqbody) {
1443 dpavlin 59 $req = HTTP::Request->new(POST => $url);
1444 dpavlin 37 } else {
1445 dpavlin 59 $req = HTTP::Request->new(GET => $url);
1446 dpavlin 37 }
1447    
1448 dpavlin 59 $req->headers->header( 'Host' => $url->host . ":" . $url->port );
1449     $req->headers->header( 'Connection', 'close' );
1450 dpavlin 77 $req->headers->header( 'Authorization', 'Basic ' . $self->{auth} ) if ($self->{auth});
1451 dpavlin 59 $req->content_type( $content_type );
1452 dpavlin 37
1453 dpavlin 59 warn $req->headers->as_string,"\n" if ($self->{debug});
1454 dpavlin 2
1455 dpavlin 37 if ($reqbody) {
1456 dpavlin 41 warn "$reqbody\n" if ($self->{debug});
1457 dpavlin 59 $req->content( $reqbody );
1458 dpavlin 33 }
1459 dpavlin 2
1460 dpavlin 59 my $res = $ua->request($req) || croak "can't make request to $url: $!";
1461 dpavlin 2
1462 dpavlin 59 warn "## response status: ",$res->status_line,"\n" if ($self->{debug});
1463 dpavlin 2
1464 dpavlin 76 ($self->{status}, $self->{status_message}) = split(/\s+/, $res->status_line, 2);
1465    
1466 dpavlin 78 if (! $res->is_success) {
1467     if ($self->{croak_on_error}) {
1468     croak("can't get $url: ",$res->status_line);
1469     } else {
1470     return -1;
1471     }
1472     }
1473 dpavlin 2
1474 dpavlin 59 $$resbody .= $res->content;
1475    
1476 dpavlin 40 warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});
1477 dpavlin 39
1478 dpavlin 40 return $self->{status};
1479 dpavlin 2 }
1480    
1481 dpavlin 48
1482 dpavlin 55 =head2 set_snippet_width
1483 dpavlin 48
1484 dpavlin 55 Set width of snippets in results
1485    
1486     $node->set_snippet_width( $wwidth, $hwidth, $awidth );
1487    
1488     C<$wwidth> specifies whole width of snippet. It's C<480> by default. If it's C<0> snippet
1489     is not sent with results. If it is negative, whole document text is sent instead of snippet.
1490    
1491     C<$hwidth> specified width of strings from beginning of string. Default
1492     value is C<96>. Negative or zero value keep previous value.
1493    
1494     C<$awidth> specifies width of strings around each highlighted word. It's C<96> by default.
1495     If negative of zero value is provided previous value is kept unchanged.
1496    
1497     =cut
1498    
1499     sub set_snippet_width {
1500     my $self = shift;
1501    
1502     my ($wwidth, $hwidth, $awidth) = @_;
1503     $self->{wwidth} = $wwidth;
1504     $self->{hwidth} = $hwidth if ($hwidth >= 0);
1505     $self->{awidth} = $awidth if ($awidth >= 0);
1506     }
1507    
1508    
1509 dpavlin 56 =head2 set_user
1510 dpavlin 55
1511 dpavlin 56 Manage users of node
1512    
1513     $node->set_user( 'name', $mode );
1514    
1515     C<$mode> can be one of:
1516    
1517     =over 4
1518    
1519     =item 0
1520    
1521     delete account
1522    
1523     =item 1
1524    
1525     set administrative right for user
1526    
1527     =item 2
1528    
1529     set user account as guest
1530    
1531     =back
1532    
1533     Return true on success, otherwise false.
1534    
1535     =cut
1536    
1537     sub set_user {
1538     my $self = shift;
1539     my ($name, $mode) = @_;
1540    
1541     return unless ($self->{url});
1542     croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);
1543    
1544     $self->shuttle_url( $self->{url} . '/_set_user',
1545     'text/plain',
1546     'name=' . uri_escape($name) . '&mode=' . $mode,
1547     undef
1548     ) == 200;
1549     }
1550    
1551    
1552 dpavlin 57 =head2 set_link
1553    
1554     Manage node links
1555    
1556     $node->set_link('http://localhost:1978/node/another', 'another node label', $credit);
1557    
1558     If C<$credit> is negative, link is removed.
1559    
1560     =cut
1561    
1562     sub set_link {
1563     my $self = shift;
1564     my ($url, $label, $credit) = @_;
1565    
1566     return unless ($self->{url});
1567     croak "mode credit be number, not '$credit'" unless ($credit =~ m/^\d+$/);
1568    
1569     my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);
1570     $reqbody .= '&credit=' . $credit if ($credit > 0);
1571    
1572     $self->shuttle_url( $self->{url} . '/_set_link',
1573 dpavlin 71 'application/x-www-form-urlencoded',
1574 dpavlin 57 $reqbody,
1575     undef
1576     ) == 200;
1577     }
1578    
1579    
1580 dpavlin 55 =head1 PRIVATE METHODS
1581    
1582     You could call those directly, but you don't have to. I hope.
1583    
1584     =head2 _set_info
1585    
1586 dpavlin 48 Set information for node
1587    
1588 dpavlin 55 $node->_set_info;
1589 dpavlin 48
1590     =cut
1591    
1592 dpavlin 55 sub _set_info {
1593 dpavlin 48 my $self = shift;
1594    
1595     $self->{status} = -1;
1596     return unless ($self->{url});
1597    
1598     my $resbody;
1599     my $rv = $self->shuttle_url( $self->{url} . '/inform',
1600     'text/plain',
1601     undef,
1602     \$resbody,
1603     );
1604    
1605     return if ($rv != 200 || !$resbody);
1606    
1607 dpavlin 58 # it seems that response can have multiple line endings
1608     $resbody =~ s/[\r\n]+$//;
1609 dpavlin 48
1610     ( $self->{name}, $self->{label}, $self->{dnum}, $self->{wnum}, $self->{size} ) =
1611     split(/\t/, $resbody, 5);
1612    
1613     }
1614    
1615 dpavlin 2 ###
1616    
1617     =head1 EXPORT
1618    
1619     Nothing.
1620    
1621     =head1 SEE ALSO
1622    
1623     L<http://hyperestraier.sourceforge.net/>
1624    
1625     Hyper Estraier Ruby interface on which this module is based.
1626    
1627     =head1 AUTHOR
1628    
1629     Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
1630    
1631    
1632     =head1 COPYRIGHT AND LICENSE
1633    
1634 dpavlin 15 Copyright (C) 2005-2006 by Dobrica Pavlinusic
1635 dpavlin 2
1636     This library is free software; you can redistribute it and/or modify
1637     it under the GPL v2 or later.
1638    
1639     =cut
1640    
1641     1;

  ViewVC Help
Powered by ViewVC 1.1.26