| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | # Copyright 2015 Google Inc. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | # you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | # You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							|  |  |  | # Copyright 2015 Google Inc. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | # you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | # You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #      http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require 'google/apis/core/multipart' | 
					
						
							|  |  |  | require 'google/apis/core/http_command' | 
					
						
							|  |  |  | require 'google/apis/core/upload' | 
					
						
							|  |  |  | require 'google/apis/core/download' | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | require 'google/apis/core/composite_io' | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | require 'addressable/uri' | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  | require 'securerandom' | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  | module Google | 
					
						
							|  |  |  |   module Apis | 
					
						
							|  |  |  |     module Core | 
					
						
							|  |  |  |       # Wrapper request for batching multiple calls in a single server request | 
					
						
							|  |  |  |       class BatchCommand < HttpCommand | 
					
						
							|  |  |  |         MULTIPART_MIXED = 'multipart/mixed' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # @param [symbol] method | 
					
						
							|  |  |  |         #   HTTP method | 
					
						
							|  |  |  |         # @param [String,Addressable::URI, Addressable::Template] url | 
					
						
							|  |  |  |         #   HTTP URL or template | 
					
						
							|  |  |  |         def initialize(method, url) | 
					
						
							|  |  |  |           super(method, url) | 
					
						
							|  |  |  |           @calls = [] | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |           @base_id = SecureRandom.uuid | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ## | 
					
						
							|  |  |  |         # Add a new call to the batch request. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @param [Google::Apis::Core::HttpCommand] call API Request to add | 
					
						
							|  |  |  |         # @yield [result, err] Result & error when response available | 
					
						
							|  |  |  |         # @return [Google::Apis::Core::BatchCommand] self | 
					
						
							|  |  |  |         def add(call, &block) | 
					
						
							|  |  |  |           ensure_valid_command(call) | 
					
						
							|  |  |  |           @calls << [call, block] | 
					
						
							|  |  |  |           self | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         protected | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ## | 
					
						
							|  |  |  |         # Deconstruct the batch response and process the individual results | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @param [String] content_type | 
					
						
							|  |  |  |         #  Content type of body | 
					
						
							|  |  |  |         # @param [String, #read] body | 
					
						
							|  |  |  |         #  Response body | 
					
						
							|  |  |  |         # @return [Object] | 
					
						
							|  |  |  |         #   Response object | 
					
						
							|  |  |  |         def decode_response_body(content_type, body) | 
					
						
							|  |  |  |           m = /.*boundary=(.+)/.match(content_type) | 
					
						
							|  |  |  |           if m | 
					
						
							|  |  |  |             parts = split_parts(body, m[1]) | 
					
						
							|  |  |  |             deserializer = CallDeserializer.new | 
					
						
							|  |  |  |             parts.each_index do |index| | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |               response = deserializer.to_http_response(parts[index]) | 
					
						
							|  |  |  |               outer_header = response.shift | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |               call_id = header_to_id(outer_header['Content-ID'].first) || index | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |               call, callback = @calls[call_id] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |               begin | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |                 result = call.process_response(*response) unless call.nil? | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |                 success(result, &callback) | 
					
						
							|  |  |  |               rescue => e | 
					
						
							|  |  |  |                 error(e, &callback) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           nil | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def split_parts(body, boundary) | 
					
						
							|  |  |  |           parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/) | 
					
						
							|  |  |  |           parts[1...-1] | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Encode the batch request | 
					
						
							|  |  |  |         # @return [void] | 
					
						
							|  |  |  |         # @raise [Google::Apis::BatchError] if batch is empty | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           fail BatchError, 'Cannot make an empty batch request' if @calls.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           serializer = CallSerializer.new | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           multipart = Multipart.new(content_type: MULTIPART_MIXED) | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |           @calls.each_index do |index| | 
					
						
							|  |  |  |             call, _ = @calls[index] | 
					
						
							|  |  |  |             content_id = id_to_header(index) | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |             io = serializer.to_part(call) | 
					
						
							|  |  |  |             multipart.add_upload(io, content_type: 'application/http', content_id: content_id) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           end | 
					
						
							|  |  |  |           self.body = multipart.assemble | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           header['Content-Type'] = multipart.content_type | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           super | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def ensure_valid_command(command) | 
					
						
							|  |  |  |           if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand) | 
					
						
							|  |  |  |             fail Google::Apis::ClientError, 'Can not include media requests in batch' | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         def id_to_header(call_id) | 
					
						
							|  |  |  |           return sprintf('<%s+%i>', @base_id, call_id) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def header_to_id(content_id) | 
					
						
							|  |  |  |           match = /<response-.*\+(\d+)>/.match(content_id) | 
					
						
							|  |  |  |           return match[1].to_i if match | 
					
						
							|  |  |  |           return nil | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Wrapper request for batching multiple uploads in a single server request | 
					
						
							|  |  |  |       class BatchUploadCommand < BatchCommand | 
					
						
							|  |  |  |         def ensure_valid_command(command) | 
					
						
							|  |  |  |           fail Google::Apis::ClientError, 'Can only include upload commands in batch' \ | 
					
						
							|  |  |  |             unless command.is_a?(Google::Apis::Core::BaseUploadCommand) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def prepare! | 
					
						
							|  |  |  |           header['X-Goog-Upload-Protocol'] = 'batch' | 
					
						
							|  |  |  |           super | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Serializes a command for embedding in a multipart batch request | 
					
						
							|  |  |  |       # @private | 
					
						
							|  |  |  |       class CallSerializer | 
					
						
							|  |  |  |         ## | 
					
						
							|  |  |  |         # Serialize a single batched call for assembling the multipart message | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @param [Google::Apis::Core::HttpCommand] call | 
					
						
							|  |  |  |         #   the call to serialize. | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @return [IO] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   the serialized request | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         def to_part(call) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           call.prepare! | 
					
						
							| 
									
										
										
										
											2019-08-19 21:16:51 +00:00
										 |  |  |           # This will add the Authorization header if needed. | 
					
						
							|  |  |  |           call.apply_request_options(call.header) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           parts = [] | 
					
						
							|  |  |  |           parts << build_head(call) | 
					
						
							|  |  |  |           parts << build_body(call) unless call.body.nil? | 
					
						
							|  |  |  |           length = parts.inject(0) { |a, e| a + e.length } | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           Google::Apis::Core::CompositeIO.new(*parts) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         protected | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def build_head(call) | 
					
						
							|  |  |  |           request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1" | 
					
						
							|  |  |  |           call.header.each do |key, value| | 
					
						
							|  |  |  |             request_head << sprintf("\r\n%s: %s", key, value) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           request_head << sprintf("\r\nHost: %s", call.url.host) | 
					
						
							|  |  |  |           request_head << "\r\n\r\n" | 
					
						
							|  |  |  |           StringIO.new(request_head) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def build_body(call) | 
					
						
							|  |  |  |           return nil if call.body.nil? | 
					
						
							|  |  |  |           return call.body if call.body.respond_to?(:read) | 
					
						
							|  |  |  |           StringIO.new(call.body) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Deconstructs a raw HTTP response part | 
					
						
							|  |  |  |       # @private | 
					
						
							|  |  |  |       class CallDeserializer | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |         # Parse a batched response. | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         # | 
					
						
							|  |  |  |         # @param [String] call_response | 
					
						
							|  |  |  |         #   the response to parse. | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @return [Array<(Fixnum, Hash, String)>] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   Status, header, and response body. | 
					
						
							|  |  |  |         def to_http_response(call_response) | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |           outer_header, outer_body = split_header_and_body(call_response) | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           status_line, payload = outer_body.split(/\n/, 2) | 
					
						
							|  |  |  |           _, status = status_line.split(' ', 3) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           header, body = split_header_and_body(payload) | 
					
						
							| 
									
										
										
										
											2015-10-19 22:36:24 +00:00
										 |  |  |           [outer_header, status.to_i, header, body] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         protected | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Auxiliary method to split the header from the body in an HTTP response. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # @param [String] response | 
					
						
							|  |  |  |         #   the response to parse. | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |         # @return [Array<(HTTP::Message::Headers, String)>] | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |         #   the header and the body, separately. | 
					
						
							|  |  |  |         def split_header_and_body(response) | 
					
						
							| 
									
										
										
										
											2016-08-17 20:51:09 +00:00
										 |  |  |           header = HTTP::Message::Headers.new | 
					
						
							| 
									
										
										
										
											2015-04-17 00:28:38 +00:00
										 |  |  |           payload = response.lstrip | 
					
						
							|  |  |  |           while payload | 
					
						
							|  |  |  |             line, payload = payload.split(/\n/, 2) | 
					
						
							|  |  |  |             line.sub!(/\s+\z/, '') | 
					
						
							|  |  |  |             break if line.empty? | 
					
						
							|  |  |  |             match = /\A([^:]+):\s*/.match(line) | 
					
						
							|  |  |  |             fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil? | 
					
						
							|  |  |  |             header[match[1]] = match.post_match | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           [header, payload] | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |