File Coverage

blib/lib/WebService/EveOnline/Cache.pm
Criterion Covered Total %
statement 21 140 15.0
branch 0 32 0.0
condition 0 29 0.0
subroutine 7 14 50.0
pod 6 6 100.0
total 34 221 15.3


line stmt bran cond sub pod time code
1             package WebService::EveOnline::Cache;
2              
3 1     1   2238 use strict;
  1         3  
  1         37  
4 1     1   5 use warnings;
  1         2  
  1         122  
5              
6             our $VERSION = "0.61";
7              
8 1     1   6 use DBI;
  1         1  
  1         54  
9 1     1   5 use Storable qw/freeze thaw/;
  1         2  
  1         115  
10 1     1   7 use Time::Local;
  1         2  
  1         67  
11              
12 1     1   3447 use Data::Dumper;
  1         10838  
  1         331  
13              
14             =head1 NAME
15              
16             WebService::EveOnline::Cache -- provide a cache for use by WebService::EveOnline
17              
18             =cut
19              
20             =head1 SYNOPSIS
21              
22             Currently, for use by WebService::EveOnline only. It makes all kinds of hideous assumptions,
23             and probably only works for SQLite databases. You can override the defaults when you
24             instantiate the WebService::EveOnline module. It is recommended that you study the source
25             code (dear no, it burns!) if you feel inclined to do this.
26              
27             It is mainly used to store the Eve Online skills tree, and cache any calls to the Eve Online
28             API so we don't keep clobbering the network every time we want to find something out.
29              
30             =cut
31              
32             =head2 new
33              
34             Instantiates the WebService::EveOnline cache. Assuming the type is SQLite (the default) it
35             attempts to open the db file if it exists.
36              
37             =cut
38              
39             sub new {
40 0     0 1   my ($class, $params) = @_;
41            
42 0           my $type = $params->{cache_type};
43 0           my $dbname = $params->{cache_dbname};
44 0   0       my $user = $params->{cache_user} || "";
45 0   0       my $pass = $params->{cache_pass} || "";
46 0   0       my $uid = $params->{eve_user_id} || "0";
47              
48 0           my ($dbh, $sql);
49            
50 0 0         unless ($type eq "no_cache") {
51 0 0 0       if (-f $dbname && $type eq "SQLite") {
52 0           $dbh = DBI->connect("dbi:$type:dbname=$dbname", "", "");
53 0           $sql = {
54             get_skill => $dbh->prepare("SELECT * FROM skill_types WHERE typeID=?"),
55             retrieve => $dbh->prepare("SELECT cachedata, cacheuntil FROM eve_cache WHERE cachekey=?"),
56             store => $dbh->prepare("INSERT INTO eve_cache (cachekey, cacheuntil, cachedata) VALUES (?, ?, ?)"),
57             map_id => $dbh->prepare("SELECT * FROM map WHERE systemID = ?"),
58             map_name => $dbh->prepare("SELECT * FROM map WHERE name = ?"),
59             delete => $dbh->prepare("DELETE FROM eve_cache WHERE cachekey = ?"),
60             };
61             }
62             }
63              
64 0           return bless({ _dbh => $dbh, _sql => $sql, _type => $type, _dbname => $dbname, _uid => $uid, _memcache => {} }, $class);
65             }
66              
67             =head2 cache_age
68              
69             Returns the age of the database cache in epoch seconds.
70              
71             =cut
72              
73             sub cache_age {
74 0     0 1   my $self = shift;
75              
76 0           my $type = $self->{_type};
77 0           my $dbname = $self->{_dbname};
78              
79 0           my $build_time = 0;
80              
81 1     1   10 no strict;
  1         2  
  1         2095  
82              
83 0           eval {
84 0 0 0       if (-f $dbname && $type eq "SQLite") {
85 0           my $dbh = DBI->connect("dbi:$type:dbname=$dbname", "", "");
86 0           my $bt = $dbh->prepare("SELECT build_epoch FROM last_build");
87 0           $bt->execute;
88 0           $build_time = $bt->fetchrow;
89 0           $bt->finish;
90             }
91             };
92            
93 0           return time - $build_time;
94             }
95              
96             =head2 repopulate
97              
98             Attempts to delete the sqlite database file and rebuild it. It should be called with the
99             data structure returned from the all_eve_skills method, i.e. the raw datastructure that
100             XML::Simple spits out. It really does nothing clever at all, and all of this code will
101             need to be significantly refactored at a later date.
102              
103             =cut
104              
105             sub repopulate {
106 0     0 1   my ($self, $hr_data) = @_;
107              
108 0   0       my $type ||= $self->{_type};
109 0   0       my $dbname ||= $self->{_dbname};
110 0           my ($dbh, $db_exists);
111              
112 0 0         if (-f $dbname) {
113 0           $db_exists = 1;
114             }
115            
116 0           eval {
117 0           $dbh = DBI->connect("dbi:$type:dbname=$dbname", "", "");
118             };
119            
120 0 0         unless ($dbh) {
121 0           warn "Problem creating cache: $@\n";
122 0           $self->{_type} = "no_cache";
123             }
124              
125 0 0         return 0 unless $dbh;
126              
127 0 0         if ($db_exists) {
128 0           foreach my $table (qw/ skill_groups skill_types skill_dependencies map last_build /) {
129 0           eval {
130 0           $dbh->do("DROP TABLE $table;");
131             };
132             }
133 0           eval {
134 0           $dbh->do("DROP INDEX map_idx;");
135             };
136             } else {
137 0           $dbh->do("CREATE TABLE eve_cache (cachekey varchar(255) not null primary key, cacheuntil int not null, cachedata text);");
138             }
139              
140 0           $dbh->do("CREATE TABLE skill_groups (groupID int not null primary key, groupName varchar(255));");
141 0           $dbh->do("CREATE TABLE skill_types (typeID int not null primary key, groupID int not null, typeName varchar(255), rank int, description text);");
142 0           $dbh->do("CREATE TABLE skill_dependencies (depID int primary key not null, typeID int not null, deptypeID int not null, level int);");
143 0           $dbh->do("CREATE TABLE last_build (build_epoch int not null);");
144 0           $dbh->do("CREATE TABLE map (systemID int not null primary key, allianceID int, constallationSovereignty int, sovereigntyLevel int, factionID int, name varchar(255));");
145 0           $dbh->do("CREATE INDEX map_idx ON map (name);");
146            
147             # this is lazy -- a cut and paste from the new sub. TODO: refactor
148 0           my $esql = {
149             get_skill => $dbh->prepare("SELECT * FROM skill_types WHERE typeID=?"),
150             retrieve => $dbh->prepare("SELECT cachedata, cacheuntil FROM eve_cache WHERE cachekey=?"),
151             store => $dbh->prepare("INSERT INTO eve_cache (cachekey, cacheuntil, cachedata) VALUES (?, ?, ?)"),
152             map_id => $dbh->prepare("SELECT * FROM map WHERE systemID = ?"),
153             map_name => $dbh->prepare("SELECT * FROM map WHERE name = ?"),
154             delete => $dbh->prepare("DELETE FROM eve_cache WHERE cachekey = ?"),
155             };
156            
157 0           my $sql = {
158             group => $dbh->prepare("INSERT INTO skill_groups VALUES (?, ?)"),
159             type => $dbh->prepare("INSERT INTO skill_types VALUES (?, ?, ?, ?, ?)"),
160             dep => $dbh->prepare("INSERT INTO skill_dependencies VALUES (?, ?, ?, ?)"),
161             lb => $dbh->prepare("INSERT INTO last_build VALUES (?)"),
162             map => $dbh->prepare("INSERT INTO map VALUES (?, ?, ?, ?, ?, ?)"),
163             };
164              
165 0           my $depid = 1;
166              
167 0           $dbh->begin_work;
168 0           foreach my $result (@{$hr_data->{skills}->{result}->{rowset}->{row}}) {
  0            
169 0           $sql->{group}->execute($result->{groupID}, $result->{groupName});
170 0           foreach my $row (@{$result->{rowset}->{row}}) {
  0            
171 0           $sql->{type}->execute($row->{typeID}, $result->{groupID}, $row->{typeName}, $row->{rank}, $row->{description});
172              
173 0           my $req = $row->{rowset}->{requiredSkills}->{row};
174 0 0         if ($req) {
175 0 0         if (ref($req) eq "ARRAY") {
176 0           foreach my $skill (@{$req}) {
  0            
177 0           $sql->{dep}->execute($depid++, $row->{typeID}, $skill->{typeID}, $skill->{skillLevel});
178             }
179             } else {
180 0           $sql->{dep}->execute($depid++, $row->{typeID}, $req->{typeID}, $req->{skillLevel});
181             }
182             }
183             }
184             }
185 0           $dbh->commit;
186 0           $dbh->begin_work;
187              
188 0           foreach my $result (@{$hr_data->{map}->{result}->{rowset}->{row}}) {
  0            
189 0           $sql->{map}->execute($result->{solarSystemID}, $result->{allianceID}, $result->{constellationSovereignity},
190             $result->{sovereignityLevel}, $result->{factionID}, $result->{solarSystemName});
191            
192             }
193 0           $sql->{lb}->execute(time);
194 0           $dbh->commit;
195            
196 0           $self->{_dbh} = $dbh;
197 0           $self->{_sql} = $esql;
198            
199 0           return 1;
200             }
201              
202             =head2 get_skill
203            
204             Returns skill data based on a typeID.
205              
206             =cut
207              
208             sub get_skill {
209 0     0 1   my ($self, $id) = @_;
210 0 0 0       return undef if $self->{type} && $self->{type} eq "no_cache";
211 0           my $sql = $self->{_sql};
212              
213 0           $sql->{get_skill}->execute($id);
214 0           return $sql->{get_skill}->fetchrow_hashref();
215             }
216              
217             =head2 retrieve
218              
219             Retrieves a previously-run command from the cache. It checks the
220             age of the cache. It will return no data if the cache has expired,
221             or if the command has not been run before.
222              
223             =cut
224              
225             sub retrieve {
226 0     0 1   my ($self, $details) = @_;
227            
228 0           my $data = undef;
229 0           my $tempdata = undef;
230 0           my $until = time - 1; # pretend cache has already expired
231 0           my $now = time;
232            
233 0           my $cachekey = $self->{_uid} . ":" . $details->{command} . ":" . $details->{params};
234              
235 0 0         if ($self->{_dbh}) {
236 0           $self->{_sql}->{retrieve}->execute($cachekey);
237 0           ($tempdata, $until) = $self->{_sql}->{retrieve}->fetchrow;
238 0           $self->{_sql}->{retrieve}->finish;
239 0 0 0       if ($tempdata && ($until >= $now)) {
240 0           $data = thaw($tempdata);
241             } else {
242 0           $self->{_sql}->{delete}->execute($cachekey);
243 0           $self->{_sql}->{delete}->finish;
244             }
245             } else {
246 0   0       $self->{_memcache}->{$cachekey} ||= [];
247 0           ($tempdata, $until) = @{$self->{_memcache}->{$cachekey}};
  0            
248 0 0 0       if ($tempdata && ($until >= $now)) {
249 0           $data = $tempdata;
250             } else {
251 0           $self->{_memcache}->{$cachekey} = [];
252             }
253             }
254            
255             # $data will only be returned if it exists and the cache on it hasn't expired
256 0           return $data;
257              
258             }
259              
260             =head2 store
261              
262             Stores the result of a command in the database cache. Returns whatever datastructure
263             is passed to it. The data is stored in Storable format.
264              
265             =cut
266              
267             sub store {
268 0     0 1   my ($self, $details) = @_;
269 0           my $cachekey = $self->{_uid} . ":" . $details->{command} . ":" . $details->{params};
270              
271 0           my $cache_until = _evedate_to_epoch($details->{cache_until});
272              
273             # The cache times we get back from Eve are usually set to 1 hour. Some things
274             # we want to cache for longer (like sex, race, etc.), and other things for
275             # shorter (account balance). So we can override it here:
276              
277 0 0         $cache_until = time + $details->{max_cache} if $details->{max_cache};
278            
279 0 0         if ($self->{_dbh}) {
280             # just to be safe, delete before insert
281 0           $self->{_sql}->{delete}->execute($cachekey);
282 0           $self->{_sql}->{delete}->finish;
283            
284 0           $self->{_sql}->{store}->execute($cachekey, $cache_until, freeze($details->{data}));
285 0           $self->{_sql}->{store}->finish;
286             } else {
287 0           $self->{_memcache}->{$cachekey} = [ $details->{data}, $cache_until ];
288             }
289              
290 0           return $details->{data};
291             }
292              
293             sub _evedate_to_epoch {
294 0     0     my ($date, $time) = split(' ', $_[0]);
295 0           my ($yr, $mo, $dy) = split('-', $date);
296 0           my ($hr, $mn, $se) = split(':', $time);
297            
298 0 0         if ($_[1]) {
299 0           return timelocal($se, $mn, $hr, $dy, --$mo, $yr);
300             } else {
301 0           return timegm($se, $mn, $hr, $dy, --$mo, $yr);
302             }
303             }
304              
305             qq/and they call it "puppy love"/;