File Coverage

blib/lib/Web/Dash/Lens.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Web::Dash::Lens;
2 2     2   9 use strict;
  2         2  
  2         62  
3 2     2   7 use warnings;
  2         2  
  2         47  
4 2     2   10 use Carp;
  2         2  
  2         134  
5 2     2   970 use Try::Tiny;
  2         2155  
  2         114  
6 2     2   1092 use Future::Q 0.012;
  2         25163  
  2         67  
7 2     2   15 use Scalar::Util qw(weaken);
  2         3  
  2         87  
8 2     2   464 use Net::DBus;
  0            
  0            
9             use Net::DBus::Reactor;
10             use Net::DBus::Annotation qw(dbus_call_noreply);
11             use Web::Dash::DeeModel;
12             use Web::Dash::Util qw(future_dbus_call);
13             use Encode;
14             use Async::Queue 0.02;
15             use utf8;
16              
17             my %SCHEMA_RESULTS = (
18             0 => 'uri',
19             1 => 'icon_hint',
20             2 => 'category_index',
21             3 => 'mimetype',
22             4 => 'name',
23             5 => 'comment',
24             6 => 'dnd_uri'
25             );
26              
27             my %SCHEMA_CATEGORIES = (
28             0 => 'name',
29             1 => 'icon_hint',
30             2 => 'renderer',
31             );
32              
33             sub new {
34             my ($class, %args) = @_;
35             my $self = bless {
36             reactor => $args{reactor} || Net::DBus::Reactor->main,
37             service_name => undef,
38             object_name => undef,
39             bus => undef,
40             bus_address => undef,
41             query_object => undef,
42             results_model_future => Future::Q->new,
43             search_hint_future => Future::Q->new,
44             categories_future => Future::Q->new,
45             request_queue => undef,
46             }, $class;
47             $self->_init_queue($args{concurrency});
48             $self->_init_bus(defined $args{bus_address} ? $args{bus_address} : ':session');
49             $self->_init_service(@args{qw(lens_file service_name object_name)});
50              
51             ## --- Procedure to connect to remote Lens service
52              
53             ## 1. Get hold of query_object
54             ## query_object is the main entry point to the Lens service in DBus.
55             ## Its service name and object name are normally obtained from .lens file.
56             ## query_object implements com.canonical.Unity.Lens interface.
57            
58             $self->{query_object} =
59             $self->{bus}->get_service($self->{service_name})->get_object($self->{object_name}, 'com.canonical.Unity.Lens');
60             {
61             ## 2. Fetch Lens meta information
62             ## We then have to obtain meta information about the lens.
63             ## query_object broadcasts such information by "Changed" signal,
64             ## so we listen to it here. "Changed" signal is emitted when
65             ## "InfoRequest" method is called on the query_object.
66            
67             weaken (my $self = $self); ## prevent memory leak
68             my $sigid; $sigid = $self->{query_object}->connect_to_signal('Changed', sub {
69             my ($result_arrayref) = @_;
70             my ($obj_name, $flag1, $flag2, $search_hint, $unknown,
71             $service_results, $service_global_results, $service_categories, $service_filters) = @$result_arrayref;
72              
73             ## 4. Obtain search_hint and some Dee Model objects
74             ## "Changed" signal conveys a number of values. I'm not able to
75             ## figure out all of their meanings. The forth value ($search_hint)
76             ## is a short description of the Lens.
77              
78             ## The last four values are DBus service names for Dee Model objects.
79             ## Lenses use these objects to export various data to DBus. Such data
80             ## include search results and categories of the results. A Dee Model
81             ## object's DBus object name is determined from the service name.
82             ## A Dee Model object is represented by Web::Dash::DeeModel class here.
83            
84             $self->{query_object}->disconnect_from_signal('Changed', $sigid);
85             $self->{search_hint_future}->fulfill(Encode::decode('utf8', $search_hint));
86              
87             ## Results Model exports Search results. We will use the Model object
88             ## later when searching.
89             $self->{results_model_future}->fulfill(Web::Dash::DeeModel->new(
90             bus => $self->{bus},
91             service_name => $service_results,
92             schema => \%SCHEMA_RESULTS,
93             ));
94              
95             ## Categories Model exports meta information about categories
96             ## of search results. Here we cache the category information,
97             ## and throw away the Model object.
98             my $categories_model = Web::Dash::DeeModel->new(
99             bus => $self->{bus},
100             service_name => $service_categories,
101             schema => \%SCHEMA_CATEGORIES,
102             );
103             $categories_model->get()->then(sub {
104             $self->{categories_future}->fulfill(@_) if defined $self;
105             }, sub {
106             $self->{categories_future}->reject(@_) if defined $self;
107             });
108             });
109             }
110            
111             ## 3. call "InfoRequest" method to make "Changed" signal fire.
112             $self->{query_object}->InfoRequest(dbus_call_noreply);
113             return $self;
114             }
115              
116             sub service_name { shift->{service_name} }
117             sub object_name { shift->{object_name} }
118              
119             sub _init_bus {
120             my ($self, $bus_address) = @_;
121             $self->{bus_address} = $bus_address;
122             if($bus_address eq ':session') {
123             $self->{bus} = Net::DBus->session;
124             }elsif($bus_address eq ':system') {
125             $self->{bus} = Net::DBus->system;
126             }else {
127             $self->{bus} = Net::DBus->new($bus_address);
128             }
129             }
130              
131             sub _remove_delims {
132             my ($str) = @_;
133             $str =~ s|^[^a-zA-Z0-9_\-\.\/]+||;
134             $str =~ s|[^a-zA-Z0-9_\-\.\/]+$||;
135             return $str;
136             }
137              
138             sub _init_service {
139             my ($self, $lens_file, $service_name, $object_name) = @_;
140             if(defined $lens_file) {
141             open my $file, "<", $lens_file or croak "Cannot read $lens_file: $!";
142             while(my $line = <$file>) {
143             chomp $line;
144             my ($key, $val) = split(/=/, $line);
145             next if not defined $val;
146             $key = _remove_delims($key);
147             $val = _remove_delims($val);
148             if($key eq 'DBusName') {
149             $self->{service_name} = $val;
150             }elsif($key eq 'DBusPath') {
151             $self->{object_name} = $val;
152             }
153             }
154             close $file;
155             }
156             $self->{service_name} = $service_name if defined $service_name;
157             $self->{object_name} = $object_name if defined $object_name;
158             if(!defined($self->{service_name}) || !defined($self->{object_name})) {
159             croak 'Specify either lens_file or combination of service_name and object_name in new()';
160             }
161             }
162              
163             sub _wait_on {
164             my ($self, $future) = @_;
165             my @result;
166             my $exception;
167             my $is_immediate = 1;
168             $future->then(sub {
169             @result = @_;
170             $self->{reactor}->shutdown if !$is_immediate;
171             }, sub {
172             $exception = shift;
173             $self->{reactor}->shutdown if !$is_immediate;
174             });
175             $is_immediate = 0;
176             $self->{reactor}->run if $future->is_pending;
177             die $exception if defined $exception;
178             return @result;
179             }
180              
181             sub search_hint {
182             my ($self) = @_;
183             return $self->{search_hint_future};
184             }
185              
186             sub search_hint_sync {
187             my ($self) = @_;
188             my ($desc) = $self->_wait_on($self->search_hint);
189             return $desc;
190             }
191              
192             sub _init_queue {
193             my ($self, $concurrency) = @_;
194              
195             ## --- Procedure of searching
196             ## Concurrency of this procedure is regulated by Async::Queue.
197            
198             weaken $self; ## prevent memory leak
199             $self->{request_queue} = Async::Queue->new(
200             concurrency => $concurrency,
201             worker => sub {
202             my ($task, $queue_done) = @_;
203             my ($query_string, $final_future) = @$task;
204             $self->{results_model_future}->then(sub {
205             ## 1. Call "Search" method on query_object with search query.
206              
207             return future_dbus_call($self->{query_object}, "Search", $query_string, {});
208             })->then(sub {
209             ## 2. Obtain search results from Results Model object
210             ## The return value of "Search" method is NOT search results.
211             ## It contains a sequence number pointing to a state of the
212             ## Results Model object. We then obtain search results from the
213             ## Results Model object. However, the current sequence number of
214             ## the Results Model may be different from the one got from
215             ## query_object. That is possible when multiple processes are
216             ## making search queries concurrently. If that happens, the
217             ## obtained search result is discarded because it is not for
218             ## the query we made.
219              
220             my ($search_result) = @_;
221             my $exp_seqnum = $search_result->{'model-seqnum'};
222             my $results_model = $self->{results_model_future}->get;
223             return $results_model->get($exp_seqnum);
224             })->then(sub {
225             my (@results) = @_;
226             $final_future->fulfill(@results);
227             $queue_done->();
228             })->catch(sub {
229             $final_future->reject(@_);
230             $queue_done->();
231             });
232             }
233             );
234             }
235              
236             sub search {
237             my ($self, $query_string) = @_;
238             my $outer_future = Future::Q->new;
239             $self->{request_queue}->push([$query_string, $outer_future]);
240             return $outer_future;
241             }
242              
243             sub search_sync {
244             my ($self, $query_string) = @_;
245             return $self->_wait_on($self->search($query_string));
246             }
247              
248             sub clone {
249             my ($self) = @_;
250             return ref($self)->new(
251             service_name => $self->service_name,
252             object_name => $self->object_name,
253             reactor => $self->{reactor},
254             bus_address => $self->{bus_address},
255             concurrency => $self->{request_queue}->concurrency,
256             );
257             }
258              
259             sub category {
260             my ($self, $category_index) = @_;
261             return $self->{categories_future}->then(sub {
262             my @categories = @_;
263             if(not defined $categories[$category_index]) {
264             die "Invalid category_index: $category_index\n";
265             }
266             return $categories[$category_index];
267             });
268             }
269              
270             sub category_sync {
271             my ($self, $category_index) = @_;
272             my ($result) = $self->_wait_on($self->category($category_index));
273             return $result;
274             }
275              
276             our $VERSION = '0.031';
277              
278             1;
279              
280             __END__