File Coverage

lib/Google/Ads/Common/ReportDownloadHandler.pm
Criterion Covered Total %
statement 66 66 100.0
branch n/a
condition n/a
subroutine 22 22 100.0
pod n/a
total 88 88 100.0


line stmt bran cond sub pod time code
1             # Copyright 2015, Google Inc. All Rights Reserved.
2             #
3             # Licensed under the Apache License, Version 2.0 (the "License");
4             # you may not use this file except in compliance with the License.
5             # You may obtain a copy of the License at
6             #
7             # http://www.apache.org/licenses/LICENSE-2.0
8             #
9             # Unless required by applicable law or agreed to in writing, software
10             # distributed under the License is distributed on an "AS IS" BASIS,
11             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12             # See the License for the specific language governing permissions and
13             # limitations under the License.
14              
15             package Google::Ads::Common::ReportDownloadHandler;
16              
17 2     2   729 use strict;
  2         4  
  2         56  
18 2     2   12 use warnings;
  2         3  
  2         49  
19 2     2   13 use utf8;
  2         4  
  2         15  
20 2     2   58 use version;
  2         5  
  2         10  
21              
22             # The following needs to be on one line because CPAN uses a particularly hacky
23             # eval() to determine module versions.
24 2     2   112 use Google::Ads::AdWords::Constants; our $VERSION = ${Google::Ads::AdWords::Constants::VERSION};
  2         4  
  2         88  
25              
26 2     2   9 use Google::Ads::AdWords::Logging;
  2         4  
  2         45  
27 2     2   270 use Google::Ads::AdWords::Reports::ReportingConfiguration;
  2         6  
  2         51  
28 2     2   236 use Google::Ads::AdWords::RequestStats;
  2         4  
  2         55  
29 2     2   269 use Google::Ads::Common::ReportDownloadError;
  2         5  
  2         49  
30 2     2   10 use Google::Ads::Common::Utilities::AdsUtilityRegistry;
  2         5  
  2         39  
31              
32 2     2   9 use Class::Std::Fast;
  2         4  
  2         12  
33              
34 2     2   537 use File::stat;
  2         5513  
  2         16  
35 2     2   133 use HTTP::Request;
  2         9  
  2         48  
36 2     2   10 use HTTP::Status qw(:constants);
  2         3  
  2         833  
37 2     2   17 use Log::Log4perl qw(:levels);
  2         4  
  2         18  
38 2     2   141 use LWP::UserAgent;
  2         5  
  2         49  
39 2     2   14 use MIME::Base64;
  2         6  
  2         102  
40 2     2   413 use POSIX;
  2         5309  
  2         19  
41 2     2   6081 use Time::HiRes qw(gettimeofday tv_interval);
  2         7  
  2         19  
42 2     2   292 use URI::Escape;
  2         5  
  2         91  
43 2     2   643 use XML::Simple;
  2         10109  
  2         16  
44              
45 2     2   141 use constant SCRUBBED_HEADERS => qw(DeveloperToken Authorization);
  2         3  
  2         2362  
46              
47             my %client_of : ATTR(:name :default<>);
48             my %__user_agent_of : ATTR(:name<__user_agent> :default<>);
49             my %__http_request_of : ATTR(:name<__http_request> :default<>);
50             my %download_format_of : ATTR(:name :default<>);
51              
52             # Returns the report contents as a string. If the report fails then returns
53             # a ReportDownloadError.
54             sub get_as_string {
55             my ($self) = @_;
56              
57             Google::Ads::Common::Utilities::AdsUtilityRegistry->add_ads_utilities(
58             "ReportDownloaderString");
59              
60             my $user_agent = $self->get___user_agent();
61              
62             $self->__set_gzip_header();
63             my $start_time = [gettimeofday()];
64              
65             my $response = $user_agent->request($self->get___http_request());
66             $response = $self->__check_response($response, $start_time);
67             if (ref $response eq "Google::Ads::Common::ReportDownloadError") {
68             return $response;
69             }
70             return $response->decoded_content();
71             }
72              
73             # Saves the report response to a file. If the report fails then returns
74             # a ReportDownloadError. Otherwise, returns the HTTPResponse.
75             sub save {
76             my ($self, $file_path) = @_;
77             if (!$file_path) {
78             warn 'No file path provided';
79             return undef;
80             }
81              
82             Google::Ads::Common::Utilities::AdsUtilityRegistry->add_ads_utilities(
83             "ReportDownloaderFile");
84              
85             my $gzip_support = $self->__set_gzip_header();
86             my $request = $self->get___http_request();
87             my $format = $self->get_download_format();
88             my $start_time = [gettimeofday()];
89             my $response;
90             ($file_path) = glob($file_path);
91             if (!$gzip_support) {
92             # If not gzip support then we can stream directly to a file.
93             $response = $self->get___user_agent()->request($request, $file_path);
94             $response = $self->__check_response($response, $start_time);
95             } else {
96             my $mode = ">:utf8";
97             if ($format =~ /^GZIPPED|PDF/) {
98             # Binary format can't dump as UTF8.
99             $mode = ">";
100             }
101             open(FH, $mode, $file_path)
102             or warn "Can't write to '$file_path': $!";
103             $response = $self->get___user_agent()->request($request);
104             $response = $self->__check_response($response, $start_time);
105             if (ref $response eq "Google::Ads::Common::ReportDownloadError") {
106             return $response;
107             }
108             # Need to decode in a file.
109             print FH $response->decoded_content();
110             close FH;
111             }
112             return $response;
113             }
114              
115             # Use this method to process results as a stream. For each chunk of data
116             # returned, the content_callback will be invoked with two arguments:
117             # $data - the chunk of data
118             # $response - the HTTP::Response
119             # If the report fails then returns a ReportDownloadError. Otherwise, returns
120             # the HTTP::Response object.
121             sub process_contents {
122             my ($self, $content_callback) = @_;
123              
124             Google::Ads::Common::Utilities::AdsUtilityRegistry->add_ads_utilities(
125             "ReportDownloaderStream");
126              
127             # Do not set the gzip header. If it is set then $content_callback will
128             # get compressed data and we don't want clients to have to deal with
129             # inflating the data.
130             my $request = $self->get___http_request();
131             my $user_agent = $self->get___user_agent();
132             my $start_time = [gettimeofday()];
133             my $response = $user_agent->request($request, $content_callback);
134             $response = $self->__check_response($response, $start_time);
135             return $response;
136             }
137              
138             # Checks the response's status code. If OK, then returns the HTTPResponse.
139             # Otherwise, returns a new ReportDownloadError.
140             sub __check_response {
141             my ($self, $response, $start_time) = @_;
142             my $is_successful = 0;
143             my $report_download_error;
144             my $return_val;
145              
146             if ($response->code == HTTP_OK) {
147             $is_successful = 1;
148             $return_val = $response;
149             } else {
150             if ($response->code == HTTP_BAD_REQUEST) {
151             $report_download_error = $self->__extract_xml_error($response);
152             } else {
153             $report_download_error = Google::Ads::Common::ReportDownloadError->new({
154             response_code => $response->code,
155             response_message => $response->message
156             });
157             }
158             $return_val = $report_download_error;
159             }
160             # Log request and response information before returning the result.
161             $self->__log_report_request_response($response, $is_successful,
162             $report_download_error, tv_interval($start_time));
163             return $return_val;
164             }
165              
166             # Sets the header and updates the user agent for gzip support if the
167             # environment supports gzip compression.
168             sub __set_gzip_header {
169             my ($self) = @_;
170             my $user_agent = $self->get___user_agent();
171              
172             my $can_accept = HTTP::Message::decodable;
173             my $gzip_support = $can_accept =~ /gzip/i;
174              
175             # Setting HTTP user-agent and gzip compression.
176             $user_agent->default_header("Accept-Encoding" => scalar $can_accept);
177              
178             # Set the header for gzip support.
179             $user_agent->agent(
180             $self->get_client()->get_user_agent() . ($gzip_support ? " gzip" : ""));
181             return $gzip_support;
182             }
183              
184             # Returns a new ReportDownloadError containing the error details of the
185             # failed HTTP::Response.
186             sub __extract_xml_error {
187             my ($self, $response) = @_;
188             my $ref =
189             XML::Simple->new()->XMLin($response->decoded_content(), ForceContent => 1);
190              
191             return Google::Ads::Common::ReportDownloadError->new({
192             response_code => $response->code,
193             response_message => $response->message,
194             type => $ref->{ApiError}->{type}->{content},
195             field_path => $ref->{ApiError}->{fieldPath}->{content}
196             ? $ref->{ApiError}->{fieldPath}->{content}
197             : "",
198             trigger => $ref->{ApiError}->{trigger}->{content}
199             ? $ref->{ApiError}->{trigger}->{content}
200             : ""
201             });
202             }
203              
204             # Logs the report request, response, and stats.
205             sub __log_report_request_response {
206             my ($self, $response, $is_successful, $error_message, $elapsed_seconds) = @_;
207              
208             my $client = $self->get_client();
209             my $request = $self->get___http_request();
210              
211             # Always log the request stats to the AdWordsAPI logger.
212             my $auth_handler = $client->_get_auth_handler();
213              
214             my $request_stats = Google::Ads::AdWords::RequestStats->new({
215             server => $client->get_alternate_url(),
216             client_id => $client->get_client_id(),
217             service_name => $request->uri,
218             method_name => $request->method,
219             is_fault => !$is_successful,
220             response_time => int(($elapsed_seconds * 1000) + 0.5),
221             fault_message => (!$is_successful) ? $response->message : ""
222             });
223             $client->_push_new_request_stats($request_stats);
224             Google::Ads::AdWords::Logging::get_awapi_logger->info($request_stats);
225              
226             # Log the request.
227             if ($request) {
228             # Log the full request:
229             # To WARN if the request failed OR
230             # To INFO if the request succeeded
231             my $request_string = $request->as_string("\n");
232             # Remove sensitive information from the log message.
233             foreach my $header (SCRUBBED_HEADERS) {
234             $request_string =~ s!(\n$header):(.*)\n!$1: REDACTED\n!;
235             }
236             my $log_message = sprintf(
237             "Outgoing request:\n%s",
238             $request_string
239             );
240             Google::Ads::AdWords::Logging::get_soap_logger->log(
241             $is_successful ? $INFO : $WARN, $log_message);
242             }
243              
244             # Log the response.
245             if ($response) {
246             # Log:
247             # To WARN if the request failed OR
248             # To INFO (status and message only)
249             my $response_string = $response->headers_as_string("\n");
250             # Remove sensitive information from the log message.
251             foreach my $header (SCRUBBED_HEADERS) {
252             $response_string =~ s!(\n$header):(.*)\n!$1: REDACTED\n!;
253             }
254             my $log_message = sprintf(
255             "Incoming %s report response with status code %s and message '%s'\n%s" .
256             "REDACTED REPORT DATA",
257             $is_successful ? 'successful' : 'failed',
258             $response->code, $response->message,
259             $response_string
260             );
261              
262             if ($is_successful) {
263             Google::Ads::AdWords::Logging::get_soap_logger->info($log_message);
264             } else {
265             if (ref $error_message eq "Google::Ads::Common::ReportDownloadError") {
266             $log_message = $log_message .
267             sprintf(
268             ": An error has occurred of type '%s', triggered by '%s'",
269             $error_message->get_type(),
270             $error_message->get_trigger());
271             } elsif ($error_message) {
272             $log_message = $log_message . ': ' . $error_message;
273             }
274             Google::Ads::AdWords::Logging::get_soap_logger->logwarn($log_message);
275             }
276             }
277             }
278              
279             1;
280              
281             =pod
282              
283             =head1 NAME
284              
285             Google::Ads::Common::ReportDownloadHandler
286              
287             =head1 DESCRIPTION
288              
289             Represents a report response from the AdWords API.
290              
291              
292             =head2 PROPERTIES
293              
294             The following properties may be accessed using get_PROPERTY methods:
295              
296             =over
297              
298             =item * client
299              
300             A reference to a Google::Ads::AdWords::Client.
301              
302             =item * __user_agent (Private)
303              
304             A reference to an LWP::UserAgent.
305              
306             =item * __http_request (Private)
307              
308             A reference to an HTTP::Request.
309              
310             =item * download_format
311              
312             The download format of the request.
313              
314             =back
315              
316              
317             =head1 METHODS
318              
319             =head2 new
320              
321             Constructor. The following data structure may be passed to new():
322              
323             { # Google::Ads::Common::ReportDownloadHandler
324             client => $response, # A ref to a Google::Ads::AdWords::Client object
325             __user_agent => $user_agent, # A ref to an LWP::UserAgent
326             __http_request => $request, # A ref to an HTTP::Request object
327             download_format => $download_format, # The download format for the request
328             },
329              
330             =head1 METHODS
331              
332             =head2 get_as_string
333              
334             Issues the report request to the AdWords API and returns the report contents
335             as a string.
336              
337             =head3 Returns
338              
339             The report contents as a string if the request is successful. Otherwise, returns
340             a L. Check for failures by evaluating
341             the return value in a boolean context, where a
342             L will always evaluate to false.
343              
344             =head3 Exceptions
345              
346             Returns a L if the report request
347             fails.
348              
349             =head2 save
350              
351             Issues the report request to the AdWords API and saves the report contents
352             to a file.
353              
354             =head3 Parameters
355              
356             =over
357              
358             =item *
359              
360             The destination file for the report contents.
361              
362             =back
363              
364             =head3 Returns
365              
366             The report contents as a string if the request is successful. Otherwise, returns
367             a L. Check for failures by
368             evaluating the return value in a boolean context, where a
369             L will always evaluate to false.
370              
371             =head3 Exceptions
372              
373             Returns a L if the report request
374             fails.
375              
376             =head2 process_contents
377              
378             Issues the report request to the AdWords API and invokes a callback for each
379             chunk of content received. Use this method to process the report contents as
380             a stream.
381              
382             =head3 Parameters
383              
384             =over
385              
386             =item *
387              
388             A content_callback that will be invoked for each chunk of data returned
389             by the report request. Each invocation will be passed two arguments:
390              
391             =over
392              
393             =item *
394              
395             The chunk of data
396              
397             =item *
398              
399             The HTTP::Response
400              
401             =back
402              
403             =back
404              
405             =head3 Returns
406              
407             An HTTP::Response if the request is successful. Otherwise, returns
408             a L. Check for failures by
409             evaluating the return value in a boolean context, where a
410             L will always evaluate to false.
411              
412             =head3 Exceptions
413              
414             Returns a L if the report request
415             fails.
416              
417             =cut
418