line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package SimpleDB::Client; |
2
|
|
|
|
|
|
|
{ |
3
|
|
|
|
|
|
|
$SimpleDB::Client::VERSION = '1.0600'; |
4
|
|
|
|
|
|
|
} |
5
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
=head1 NAME |
7
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
SimpleDB::Client - The network interface to the SimpleDB service. |
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
=head1 VERSION |
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
version 1.0600 |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
=head1 SYNOPSIS |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
use SimpleDB::Client; |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
my $sdb = SimpleDB::Client->new(secret_key=>'abc', access_key=>'123'); |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
# create a domain |
21
|
|
|
|
|
|
|
my $hashref = $sdb->send_request('CreateDomain', {DomainName => 'my_things'}); |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
# insert attributes |
24
|
|
|
|
|
|
|
my $hashref = $sdb->send_request('PutAttributes', { |
25
|
|
|
|
|
|
|
DomainName => 'my_things', |
26
|
|
|
|
|
|
|
ItemName => 'car', |
27
|
|
|
|
|
|
|
'Attribute.1.Name' => 'color', |
28
|
|
|
|
|
|
|
'Attribute.1.Value' => 'red', |
29
|
|
|
|
|
|
|
'Attribute.1.Replace' => 'true', |
30
|
|
|
|
|
|
|
}); |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
# get attributes |
33
|
|
|
|
|
|
|
my $hashref = $sdb->send_request('GetAttributes', { |
34
|
|
|
|
|
|
|
DomainName => 'my_things', |
35
|
|
|
|
|
|
|
ItemName => 'car', |
36
|
|
|
|
|
|
|
}); |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
# search attributes |
39
|
|
|
|
|
|
|
my $hashref = $sdb->send_request('Select', { |
40
|
|
|
|
|
|
|
SelectExpression => q{select * from my_things where color = 'red'}, |
41
|
|
|
|
|
|
|
}); |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
=head1 DESCRIPTION |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
This class will let you quickly and easily inteface with AWS SimpleDB. It throws exceptions from L<SimpleDB::Client::Exception>. It's very light weight. Although we haven't run any benchmarks on the other modules, it should outperform any of the other Perl modules that exist today. |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=head1 METHODS |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
The following methods are available from this class. |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
=cut |
52
|
|
|
|
|
|
|
|
53
|
1
|
|
|
1
|
|
1027
|
use Moose; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
use Digest::SHA qw(hmac_sha256_base64); |
55
|
|
|
|
|
|
|
use XML::Fast; |
56
|
|
|
|
|
|
|
use LWP::UserAgent; |
57
|
|
|
|
|
|
|
use HTTP::Request; |
58
|
|
|
|
|
|
|
use Time::HiRes qw(usleep); |
59
|
|
|
|
|
|
|
use URI::Escape qw(uri_escape_utf8); |
60
|
|
|
|
|
|
|
use SimpleDB::Client::Exception; |
61
|
|
|
|
|
|
|
use URI; |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
#-------------------------------------------------------- |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=head2 new ( params ) |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=head3 params |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
A hash containing the parameters to pass in to this method. |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
=head4 access_key |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
The access key given to you from Amazon when you sign up for the SimpleDB service at this URL: L<http://aws.amazon.com/simpledb/> |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
=head4 secret_key |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
The secret access key given to you from Amazon. |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
=head4 simpledb_uri |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
The constructor that SimpleDB::Client will connect to. Defaults to: |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
URI->new('https://sdb.amazonaws.com/') |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
=cut |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
#-------------------------------------------------------- |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=head2 access_key ( ) |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Returns the access key passed to the constructor. |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=cut |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
has 'access_key' => ( |
96
|
|
|
|
|
|
|
is => 'ro', |
97
|
|
|
|
|
|
|
required => 1, |
98
|
|
|
|
|
|
|
documentation => 'The AWS SimpleDB access key id provided by Amazon.', |
99
|
|
|
|
|
|
|
); |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
#-------------------------------------------------------- |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
=head2 secret_key ( ) |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
Returns the secret key passed to the constructor. |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
=cut |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
has 'secret_key' => ( |
110
|
|
|
|
|
|
|
is => 'ro', |
111
|
|
|
|
|
|
|
required => 1, |
112
|
|
|
|
|
|
|
documentation => 'The AWS SimpleDB secret access key id provided by Amazon.', |
113
|
|
|
|
|
|
|
); |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
#-------------------------------------------------------- |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=head2 simpledb_uri ( ) |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
Returns the L<URI> object passed into the constructor that SimpleDB::Client will connect to. Defaults to: |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
URI->new('https://sdb.amazonaws.com/') |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
=cut |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
has simpledb_uri => ( |
126
|
|
|
|
|
|
|
is => 'ro', |
127
|
|
|
|
|
|
|
default => sub { URI->new('http://sdb.amazonaws.com/') }, |
128
|
|
|
|
|
|
|
); |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
#-------------------------------------------------------- |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
=head2 user_agent ( ) |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
Returns the L<LWP::UserAgent> object that is used to connect to SimpleDB. It's cached here so it doesn't have to be created each time. |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
=cut |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
has user_agent => ( |
139
|
|
|
|
|
|
|
is => 'ro', |
140
|
|
|
|
|
|
|
default => sub { LWP::UserAgent->new(timeout=>30, keep_alive=>1); }, |
141
|
|
|
|
|
|
|
); |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
#-------------------------------------------------------- |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
=head2 construct_request ( action, [ params ] ) |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
Returns a string that contains the HTTP post data ready to make a request to SimpleDB. Normally this is only called by send_request(), but if you want to debug a SimpleDB interaction, then having access to this method is critical. |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=head3 action |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
The action to perform on SimpleDB. See the "Operations" section of the guide located at L<http://docs.amazonwebservices.com/AmazonSimpleDB/2009-04-15/DeveloperGuide/>. |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
=head3 params |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
Any extra prameters required by the operation. The normal parameters of Action, AWSAccessKeyId, Version, Timestamp, SignatureMethod, SignatureVersion, and Signature are all automatically provided by this method. |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
=cut |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
sub construct_request { |
160
|
|
|
|
|
|
|
my ($self, $action, $params) = @_; |
161
|
|
|
|
|
|
|
my $encoding_pattern = "^A-Za-z0-9\-_.~"; |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
# add required parameters |
164
|
|
|
|
|
|
|
$params->{'Action'} = $action; |
165
|
|
|
|
|
|
|
$params->{'AWSAccessKeyId'} = $self->access_key; |
166
|
|
|
|
|
|
|
$params->{'Version'} = '2009-04-15'; |
167
|
|
|
|
|
|
|
$params->{'Timestamp'} = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3], $_[2], $_[1], $_[0]) }->(gmtime(time))); |
168
|
|
|
|
|
|
|
$params->{'SignatureMethod'} = 'HmacSHA256'; |
169
|
|
|
|
|
|
|
$params->{'SignatureVersion'} = 2; |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
# construct post data |
172
|
|
|
|
|
|
|
my $post_data; |
173
|
|
|
|
|
|
|
foreach my $name (sort {$a cmp $b} keys %{$params}) { |
174
|
|
|
|
|
|
|
$post_data .= $name . '=' . uri_escape_utf8($params->{$name}, $encoding_pattern) . '&'; |
175
|
|
|
|
|
|
|
} |
176
|
|
|
|
|
|
|
chop $post_data; |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
# sign the post data |
179
|
|
|
|
|
|
|
my $signature = "POST\n".$self->simpledb_uri->host."\n/\n". $post_data; |
180
|
|
|
|
|
|
|
$signature = hmac_sha256_base64($signature, $self->secret_key) . '='; |
181
|
|
|
|
|
|
|
$post_data .= '&Signature=' . uri_escape_utf8($signature, $encoding_pattern); |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
my $request = HTTP::Request->new('POST', $self->simpledb_uri->as_string); |
184
|
|
|
|
|
|
|
$request->content_type("application/x-www-form-urlencoded; charset=utf-8"); |
185
|
|
|
|
|
|
|
$request->content($post_data); |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
return $request; |
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
#-------------------------------------------------------- |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
=head2 send_request ( action, [ params ] ) |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
Creates a request, and then sends it to SimpleDB. The response is returned as a hash reference of the raw XML document returned by SimpleDB. Automatically attempts 5 cascading retries on connection failure. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
Throws SimpleDB::Client::Exception::Response and SimpleDB::Client::Exception::Connection. |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
=head3 action |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
See create_request() for details. |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
=head3 params |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
See create_request() for details. |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
=cut |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
sub send_request { |
209
|
|
|
|
|
|
|
my ($self, $action, $params) = @_; |
210
|
|
|
|
|
|
|
my $request = $self->construct_request($action, $params); |
211
|
|
|
|
|
|
|
# loop til we get a response or throw an exception |
212
|
|
|
|
|
|
|
foreach my $retry (1..5) { |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
# make the request |
215
|
|
|
|
|
|
|
my $ua = $self->user_agent; |
216
|
|
|
|
|
|
|
my $response = $ua->request($request); |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
# got a possibly recoverable error, let's retry |
219
|
|
|
|
|
|
|
if ($response->code >= 500 && $response->code < 600) { |
220
|
|
|
|
|
|
|
if ($retry < 5) { |
221
|
|
|
|
|
|
|
usleep((4 ** $retry) * 100_000); |
222
|
|
|
|
|
|
|
} |
223
|
|
|
|
|
|
|
else { |
224
|
|
|
|
|
|
|
warn $response->header('Reason'); |
225
|
|
|
|
|
|
|
SimpleDB::Client::Exception::Connection->throw(error=>'Exceeded maximum retries.', status_code=>$response->code); |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
# not a retry |
230
|
|
|
|
|
|
|
else { |
231
|
|
|
|
|
|
|
return $self->handle_response($response); |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
} |
234
|
|
|
|
|
|
|
} |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
#-------------------------------------------------------- |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
=head2 handle_response ( response ) |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
Returns a hashref containing the response from SimpleDB. |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
Throws SimpleDB::Client::Exception::Response. |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=head3 response |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
The L<HTTP::Response> object created by the C<send_request> method. |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
=cut |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
sub handle_response { |
251
|
|
|
|
|
|
|
my ($self, $response) = @_; |
252
|
|
|
|
|
|
|
my $tree = eval { xml2hash($response->content) }; |
253
|
|
|
|
|
|
|
my (undef, $content) = each %$tree; # discard root like XMLin |
254
|
|
|
|
|
|
|
# compatibility with SimpleDB::Class |
255
|
|
|
|
|
|
|
if (exists $content->{SelectResult} && ! $content->{SelectResult}) { |
256
|
|
|
|
|
|
|
$content->{SelectResult} = {}; |
257
|
|
|
|
|
|
|
} |
258
|
|
|
|
|
|
|
# force an item list into an array |
259
|
|
|
|
|
|
|
if (exists $content->{SelectResult}{Item} && ref $content->{SelectResult}{Item} ne 'ARRAY') { |
260
|
|
|
|
|
|
|
$content->{SelectResult}{Item} = [ $content->{SelectResult}{Item} ]; |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# choked reconstituing the XML, probably because it wasn't XML |
264
|
|
|
|
|
|
|
if ($@) { |
265
|
|
|
|
|
|
|
SimpleDB::Client::Exception::Response->throw( |
266
|
|
|
|
|
|
|
error => 'Response was garbage. Confirm Net::SSLeay, XML::Parser, and XML::Simple installations.', |
267
|
|
|
|
|
|
|
status_code => $response->code, |
268
|
|
|
|
|
|
|
response => $response, |
269
|
|
|
|
|
|
|
); |
270
|
|
|
|
|
|
|
} |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
# got a valid response |
273
|
|
|
|
|
|
|
elsif ($response->is_success) { |
274
|
|
|
|
|
|
|
return $content; |
275
|
|
|
|
|
|
|
} |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
# SimpleDB gave us an error message |
278
|
|
|
|
|
|
|
else { |
279
|
|
|
|
|
|
|
SimpleDB::Client::Exception::Response->throw( |
280
|
|
|
|
|
|
|
error => $content->{Errors}{Error}{Message}, |
281
|
|
|
|
|
|
|
status_code => $response->code, |
282
|
|
|
|
|
|
|
error_code => $content->{Errors}{Error}{Code}, |
283
|
|
|
|
|
|
|
box_usage => $content->{Errors}{Error}{BoxUsage}, |
284
|
|
|
|
|
|
|
request_id => $content->{RequestID}, |
285
|
|
|
|
|
|
|
response => $response, |
286
|
|
|
|
|
|
|
); |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
} |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
=head1 PREREQS |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
This package requires the following modules: |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
L<XML::Fast> |
295
|
|
|
|
|
|
|
L<LWP> |
296
|
|
|
|
|
|
|
L<Time::HiRes> |
297
|
|
|
|
|
|
|
L<Crypt::SSLeay> |
298
|
|
|
|
|
|
|
L<Moose> |
299
|
|
|
|
|
|
|
L<Digest::SHA> |
300
|
|
|
|
|
|
|
L<URI> |
301
|
|
|
|
|
|
|
L<Exception::Class> |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
=head1 SUPPORT |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
=over |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
=item Repository |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
L<http://github.com/rizen/SimpleDB-Client> |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
=item Bug Reports |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
L<http://rt.cpan.org/Public/Dist/Display.html?Name=SimpleDB-Client> |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
=back |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
=head1 SEE ALSO |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
There are other packages you can use to access SimpleDB. I chose not to use them because I wanted something a bit more lightweight that I could build L<SimpleDB::Class> on top of so I could easily map objects to SimpleDB Domain Items. If you're looking for a low level SimpleDB accessor and for some reason this module doesn't cut the mustard, then you should check out these: |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
=over |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
=item Amazon::SimpleDB (L<http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1136>) |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
A complete and nicely functional low level library made by Amazon itself. |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=item L<Amazon::SimpleDB> |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
A low level SimpleDB accessor that's in its infancy and may be abandoned, but appears to be pretty functional, and of the same scope as Amazon's own module. |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=back |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
In addition to clients, there is at least one other API compatible server out there that basically lets you host your own SimpleDB if you don't want to put it in Amazon's cloud. It's called M/DB. You can read more about it here: L<http://gradvs1.mgateway.com/main/index.html?path=mdb>. Though I haven't tested it, since it's API compatible, you should be able to use it with both this module and L<SimpleDB::Class>. |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=head1 AUTHOR |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
JT Smith <jt_at_plainblack_com> |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
I have to give credit where credit is due: SimpleDB::Client is heavily inspired by the Amazon::SimpleDB class distributed by Amazon itself (not to be confused with L<Amazon::SimpleDB> written by Timothy Appnel). |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
=head1 LEGAL |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
SimpleDB::Client is Copyright 2009-2010 Plain Black Corporation (L<http://www.plainblack.com/>) and is licensed under the same terms as Perl itself. |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
=cut |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
no Moose; |
348
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |