1 package OpenILS::WWW::AutoSuggest;
7 use Apache2::Const -compile => qw(
8 OK HTTP_NOT_ACCEPTABLE HTTP_INTERNAL_SERVER_ERROR :log
12 use CGI qw(:all -utf8);
13 use Digest::MD5 qw(md5_hex);
15 use OpenSRF::Utils::JSON;
16 use OpenILS::Utils::CStoreEditor qw/:funcs/;
17 use OpenSRF::Utils::Logger qw/:level/;
18 use OpenSRF::Utils::SettingsClient;
19 use OpenSRF::Utils::Cache;
21 my $log = 'OpenSRF::Utils::Logger';
29 my $conf = OpenSRF::Utils::SettingsClient->new;
31 $cache_timeout = $conf->config_value(
32 "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
36 $cache = OpenSRF::Utils::Cache->new('global');
40 # BEGIN package globals
42 # We'll probably never need this fanciness for autosuggest, but
43 # you can add handlers for different requested content-types here, and
44 # you can weight them to control what matches requests for things like
47 my $_output_handler_dispatch = {
48 "application/xml" => {
52 $r->content_type("application/xml; charset=utf-8");
53 print suggestions_to_xml($data);
54 return Apache2::Const::OK;
57 "application/json" => {
61 $r->content_type("application/json; charset=utf-8");
62 print suggestions_to_json($data);
63 return Apache2::Const::OK;
68 my @_output_handler_types = sort {
69 $_output_handler_dispatch->{$a}->{prio} <=>
70 $_output_handler_dispatch->{$b}->{prio}
71 } keys %$_output_handler_dispatch;
75 # The third argument to our stored procedure, metabib.suggest_browse_entries(),
76 # is passed through directly to ts_headline() as the 'options' arugment.
77 sub prepare_headline_opts {
78 my ($css_prefix, $highlight_min, $highlight_max, $short_word_length) = @_;
80 $css_prefix =~ s/[^\w]//g;
83 qq{StartSel="<span class='$css_prefix'>"},
87 push @parts, "MinWords=$highlight_min" if $highlight_min > 0;
88 push @parts, "MaxWords=$highlight_max" if $highlight_max > 0;
89 push @parts, "ShortWord=$short_word_length" if defined $short_word_length;
91 return join(", ", @parts);
94 # Get raw autosuggest data (rows returned from a stored procedure) from the DB.
98 my $search_class = shift;
100 my $css_prefix = shift || 'oils_AS';
101 my $highlight_min = int(shift || 0);
102 my $highlight_max = int(shift || 0);
103 my $short_word_length = shift;
105 my $normalization = int(shift || 14); # 14 is not totally arbitrary.
106 # See http://www.postgresql.org/docs/9.0/static/textsearch-controls.html#TEXTSEARCH-RANKING
108 my $limit = int(shift || 10);
110 $limit = 10 unless $limit > 0;
112 my $headline_opts = prepare_headline_opts(
113 $css_prefix, $highlight_min, $highlight_max,
114 defined $short_word_length ? int($short_word_length) : undef
117 my $key = 'oils_AS_' . md5_hex(
129 my $res = $cache->get_cache( $key );
131 return $res if ($res);
133 $res = $editor->json_query({
135 "metabib.suggest_browse_entries",
145 $cache->put_cache( $key => $res => $cache_timeout );
150 sub suggestions_to_xml {
151 my ($suggestions) = @_;
153 my $dom = new XML::LibXML::Document("1.0", "UTF-8");
154 my $as = $dom->createElement("as");
155 $dom->setDocumentElement($as);
157 foreach (@$suggestions) {
158 my $val = $dom->createElement("val");
159 $val->setAttribute("term", $_->{value});
160 $val->setAttribute("field", $_->{field});
161 $val->appendText($_->{match});
165 # XML::LibXML::Document::toString() returns an encoded byte string, which
166 # is why we don't need to binmode STDOUT, ':utf8'.
167 return $dom->toString();
170 sub suggestions_to_json {
171 my ($suggestions) = @_;
173 return OpenSRF::Utils::JSON->perl2JSON({
176 +{ term => $_->{value}, field => $_->{field},
177 match => $_->{match} }
183 # Given data and the Apache request object, this sub picks a sub from a
184 # dispatch table based on the list of content-type encodings that the client
185 # has indicated it will accept, and calls that sub, which will deliver
186 # a response of appropriately encoded data.
190 foreach my $media_range (split /,/, $r->headers_in->{Accept}) {
191 $media_range =~ s/;.+$//; # keep type, subtype. lose parameters.
194 Text::Glob::match_glob($media_range, $_)
195 } @_output_handler_types;
198 return $_output_handler_dispatch->{$match}{code}->($r, $data);
202 return Apache2::Const::HTTP_NOT_ACCEPTABLE;
206 child_init() unless $init_done;
211 my $editor = new_editor;
212 my $suggestions = get_suggestions(
214 map { scalar($cgi->param($_)) } qw(
227 if (not $suggestions) {
229 "get_suggestions() failed: " . $editor->die_event->{textcode}
231 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
236 return output_handler($r, $suggestions);