File Coverage

blib/lib/Mobile/UserAgentFactory.pm
Criterion Covered Total %
statement 12 127 9.4
branch 0 50 0.0
condition 0 33 0.0
subroutine 4 13 30.7
pod 1 1 100.0
total 17 224 7.5


line stmt bran cond sub pod time code
1             package Mobile::UserAgentFactory;
2 1     1   12859 use strict;
  1         4  
  1         89  
3 1     1   2373 use Mobile::UserAgent;
  1         5  
  1         59  
4 1     1   14 use base qw(Class::Singleton);
  1         2  
  1         6052  
5             our $VERSION = sprintf "%d.%02d", q$Revision: 1.1 $ =~ m/ (\d+) \. (\d+) /xg;
6            
7            
8             # Contructor called by Class::Singleton to initialize a new instance.
9             sub _new_instance {
10 0     0     my $proto = shift;
11 0           my $options = shift;
12 0   0       my $class = ref($proto) || $proto;
13 0           my %cache_options;
14 0 0 0       if (defined($options) && (ref($options) eq 'HASH')) {
15 0 0         if (defined($options->{'cache_expires_in'})) {
16 0           $cache_options{'expires_in'} = $options->{'cache_expires_in'};
17             }
18 0 0         if (defined($options->{'cache_purge_interval'})) {
19 0           $cache_options{'purge_interval'} = $options->{'cache_purge_interval'};
20             }
21 0 0         if (defined($options->{'cache_max_age'})) {
22 0           $cache_options{'max_age'} = $options->{'cache_max_age'};
23             }
24 0 0         if (defined($options->{'cache_max_objects'})) {
25 0           $cache_options{'max_objects'} = $options->{'cache_max_objects'};
26             }
27             }
28 0           my $self = {
29             'cache' => Mobile::UserAgentFactoryCache->new(\%cache_options), # internal class
30             };
31 0           bless $self,$class;
32 0           return $self;
33             }
34            
35            
36            
37             # Uses the given useragent string to return a Mobile::UserAgent object if a match can be found.
38             sub getMobileUserAgent {
39 0     0 1   my $self = shift;
40 0           my $useragent;
41 0           my $debug = 0;
42 0 0         if (@_) {
43 0 0         if (ref($_[0]) eq '') {
    0          
44 0           $useragent = shift;
45             }
46             elsif (UNIVERSAL::isa(ref($_[0]), 'CGI')) {
47 0           my $q = shift;
48 0           $useragent = $q->user_agent();
49             }
50             }
51 0 0 0       if (@_ && (ref($_[0]) eq 'HASH')) {
52 0           my $options = shift;
53 0 0         if (defined($options->{'debug'})) {
54 0           $debug = $options->{'debug'};
55             }
56             }
57 0 0         unless(defined($useragent)) {
58 0           $useragent = $ENV{'HTTP_USER_AGENT'};
59 0 0         unless(defined($useragent)) {
60 0 0         $debug && print("Returning undef, because no user-agent was found in env vars.\n");
61 0           return undef;
62             }
63             }
64            
65             # Try to fetch object from internal cache.
66 0           my $cache = $self->{'cache'};
67 0           my $mua = $cache->get($useragent);
68 0 0 0       if (defined($mua) || $cache->key_exists($useragent)) {
69 0 0         $debug && print("Returning Mobile::UserAgent object found in internal cache.\n");
70 0           return $mua;
71             }
72            
73             # Create new Mobile::UserAgent object, cache it, and return it.
74 0           $mua = Mobile::UserAgent->new($useragent);
75 0           $cache->set($useragent, $mua);
76 0           return $mua;
77             }
78            
79             1;
80            
81             #### end of Mobile::UserAgentFactory ####
82            
83            
84            
85            
86            
87            
88            
89            
90            
91            
92            
93             # Internal cache manager class.
94             package Mobile::UserAgentFactoryCache;
95 1     1   1029 use strict;
  1         3  
  1         948  
96            
97            
98             # Contructor. Accepts an optional hash ref of options.
99             sub new {
100 0     0     my $proto = shift;
101 0           my $options = shift;
102 0   0       my $class = ref($proto) || $proto;
103 0           my $expires_in = 86400; # 1 day
104 0           my $purge_interval = 3600; # 1 hour
105 0           my $max_age = 604800; # 1 week
106 0           my $max_objects = 1000;
107 0 0 0       if (defined($options) && (ref($options) eq 'HASH')) {
108 0 0 0       if (defined($options->{'expires_in'}) && $options->{'expires_in'}) {
109 0           $expires_in = $options->{'expires_in'};
110             }
111 0 0 0       if (defined($options->{'purge_interval'}) && $options->{'purge_interval'}) {
112 0           $purge_interval = $options->{'purge_interval'};
113             }
114 0 0 0       if (defined($options->{'max_age'}) && $options->{'max_age'}) {
115 0           $max_age = $options->{'max_age'};
116             }
117 0 0 0       if (defined($options->{'max_objects'}) && $options->{'max_objects'}) {
118 0           $max_objects = $options->{'max_objects'};
119             }
120             }
121 0           my $self = {
122             'objects' => {}, # Cache of key => [object, create-time, last-access-time]
123             'expires_in' => $expires_in,
124             'purge_interval' => $purge_interval,
125             'max_age' => $max_age,
126             'max_objects' => $max_objects,
127             'last_purge' => time,
128             'max_objects_check_interval' => int($max_objects / 10), # after this many set() calls, the limit_max_objects() call will be executed.
129             'max_objects_set_counter' => 0, # increases with each set() method call and is reset with with each limit_max_objects() call.
130             };
131 0           bless $self,$class;
132 0           return $self;
133             }
134            
135            
136             # Checks if a key exists in the cache.
137             sub key_exists {
138 0     0     my $self = shift;
139 0           my $key = shift;
140 0           return exists($self->{'objects'}->{$key});
141             }
142            
143            
144             # Gets a cached object.
145             sub get {
146 0     0     my $self = shift;
147 0           my $key = shift;
148 0           my $objects = $self->{'objects'};
149 0           my $result;
150 0 0         if (exists($objects->{$key})) {
151 0           my $object = $objects->{$key};
152 0           $result = $object->[0];
153 0           $object->[2] = time;
154             }
155 0           $self->_purge();
156 0           return $result;
157             }
158            
159            
160             # Simply calls purge() if it's time to do so.
161             sub _purge {
162 0     0     my $self = shift;
163 0 0         if ($self->{'last_purge'} + $self->{'purge_interval'} <= time) {
164 0           return $self->purge();
165             }
166 0           return 0;
167             }
168            
169            
170             # Purges all cached objects that have not been accessed recently or are too old.
171             sub purge {
172 0     0     my $self = shift;
173 0           my $objects = $self->{'objects'};
174 0           my $now = time;
175 0           my $max_age = $self->{'max_age'};
176 0           my $expires = $self->{'expires_in'};
177 0           my $result = 0;
178 0           foreach my $key (keys %{$objects}) {
  0            
179 0           my $object = $objects->{$key};
180 0 0 0       if (($object->[2] + $expires <= $now) || ($object->[1] + $max_age <= $now)) {
181 0           print "About to purge key: $key\n";
182 0           delete($objects->{$key});
183 0           $result++;
184             }
185             }
186 0           $self->{'last_purge'} = $now;
187 0           return $result;
188             }
189            
190            
191             # Sets a new object.
192             sub set {
193 0     0     my $self = shift;
194 0           my $key = shift;
195 0           my $object = shift;
196 0           my $now = time;
197 0           $self->{'objects'}->{$key} = [$object, $now, $now];
198 0 0         if (++$self->{'max_objects_set_counter'} >= $self->{'max_objects_check_interval'}) {
199 0           return $self->limit_max_objects();
200             }
201             }
202            
203            
204             # Shrinks the cache to 10% below max if max has been exceeded by 10%.
205             sub limit_max_objects {
206 0     0     my $self = shift;
207 0           $self->_purge();
208 0           my $objects = $self->{'objects'};
209 0           my $size = scalar(keys(%{$objects}));
  0            
210 0           my $max_objects = $self->{'max_objects'};
211 0 0         if ($size <= $max_objects) {
212 0           return 0;
213             }
214             # sort keys on last-access-time descending
215 0           my @sorted_keys = sort { $objects->{$b}->[2] <=> $objects->{$a}->[2] } keys(%{$objects});
  0            
  0            
216 0           my @expired_keys = splice(@sorted_keys, $max_objects - 1 - int(0.2 * $max_objects)); # shrink to 20% below max
217             #print 'About to delete keys: ' . join(' ', @expired_keys) . "\n";
218 0           foreach my $key (@expired_keys) {
219 0           delete($objects->{$key});
220             }
221 0           $self->{'max_objects_set_counter'} = 0;
222 0           return scalar(@expired_keys);
223             }
224            
225             #### end of Mobile::UserAgentFactoryCache ####
226             1;
227            
228            
229             __END__