atlas_middleware 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ require 'arcserver'
2
+
3
+ class MapServerLegendInfo
4
+ def call(env)
5
+ request = Rack::Request.new(env)
6
+
7
+ begin
8
+ map_server = ArcServer::MapServer.new(request['url'])
9
+ map_name = map_server.get_default_map_name
10
+ legend = map_server.get_default_map_name(:map_name => map_name)
11
+ rescue Exception => e
12
+ return [500, { "Content-Type" => "text/plain" }, e.message]
13
+ end
14
+
15
+ [200, { "Content-Type" => "application/json" }, legend.to_json]
16
+ end
17
+ end
@@ -0,0 +1,206 @@
1
+ #! /usr/bin/ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__))
4
+
5
+ require 'rubygems'
6
+ require 'prawn'
7
+ require 'prawn/format'
8
+ require 'prawn/layout'
9
+ #require 'prawn/fast_png'
10
+ require 'open-uri'
11
+ require 'esri/soap/map_server/map_server'
12
+ require 'RMagick'
13
+ require 'perftools'
14
+ require 'base64'
15
+
16
+ module Prawn
17
+ module Images
18
+ class PNG
19
+ alias_method :prawn_fast_png_old_initialize, :initialize
20
+
21
+ def initialize(data) #:nodoc:
22
+ @prawn_fast_png_data = data
23
+ prawn_fast_png_old_initialize(data)
24
+ end
25
+
26
+ private
27
+ def unfilter_image_data
28
+ img = Magick::Image.from_blob(@prawn_fast_png_data).first
29
+
30
+ # get only one color value per pixel (Intensity) for grayscale+alpha images
31
+ format = color_type == 4 ? 'I' : 'RGB'
32
+
33
+ img_data = img.export_pixels_to_str(0, 0, width, height, format)
34
+ alpha_channel = img.export_pixels_to_str(0, 0, width, height, 'A')
35
+
36
+ img.destroy!
37
+ @prawn_fast_png_data = nil
38
+
39
+ @img_data = Zlib::Deflate.deflate(img_data)
40
+ @alpha_channel = Zlib::Deflate.deflate(alpha_channel)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ def collect_legend_images
48
+ images = []
49
+ service = 'http://sampleserver1.arcgisonline.com/ArcGIS/services/Portland/ESRI_LandBase_WebMercator/MapServer'
50
+ legend_infos = ESRI::Soap::MapServer.get_legend_info(service, :image_return_type => :data)
51
+
52
+ legend_infos.each do |legend_info|
53
+ if legend_info.legendGroups[0].legendClasses.length == 1
54
+ img_data = Base64.decode64(Base64.decode64(legend_info.legendGroups[0].legendClasses[0].symbolImage.imageData))
55
+ images << {
56
+ :label => legend_info.name,
57
+ :image => Magick::Image.from_blob(img_data).first
58
+ }
59
+ else
60
+ legend_info.legendGroups[0].legendClasses.each do |legend_class|
61
+ img_data = Base64.decode64(Base64.decode64(legend_class.symbolImage.imageData))
62
+ images << {
63
+ :label => legend_class.label,
64
+ :image => Magick::Image.from_blob(img_data).first
65
+ }
66
+ end
67
+ end
68
+ end
69
+
70
+ images
71
+ end
72
+
73
+ def create_legend(pdf, opts = {})
74
+ images = collect_legend_images
75
+
76
+ columns = opts[:columns] || 4
77
+ rows = opts[:rows] || 4
78
+ gutter = opts[:gutter] || 0
79
+
80
+ pdf.define_grid(:columns => columns, :rows => rows, :gutter => gutter)
81
+
82
+ pdf.grid.rows.times do |i|
83
+ pdf.grid.columns.times do |j|
84
+ cell = pdf.grid(i,j)
85
+ pdf.bounding_box cell.top_left, :width => cell.width, :height => cell.height do
86
+ return if images.empty?
87
+ image = images.shift
88
+
89
+ pdf.image StringIO.new(image[:image].to_blob), :at => [pdf.bounds.top_left], :fit => [16, 16]
90
+ pdf.text_box image[:label],
91
+ :width => cell.width,
92
+ :height => cell.height,
93
+ :overflow => :ellipses,
94
+ :at => [pdf.bounds.left + 20, pdf.bounds.top - 2]
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def portrait_pdf
101
+ print 'creating portrait pdf...'
102
+ Prawn::Document.generate '/home/colin/portrait.pdf', :page_layout => :portrait do |pdf|
103
+ box = pdf.bounds
104
+ # html tag definitions
105
+ pdf.tags :h1 => { :font_size => '25', :font_style => :bold }
106
+ # html style definitions
107
+ pdf.styles :footer => { :color => "#999", :font_style => :italic }
108
+
109
+ # header
110
+ pdf.bounding_box [box.left, box.top], :width => box.right, :height => 30 do
111
+ pdf.text "<h1>Title</h1>"
112
+ end
113
+
114
+ # map
115
+ pdf.bounding_box [box.left, pdf.cursor], :width => box.right, :height => box.width do
116
+ pdf.stroke_bounds
117
+ pdf.image "/home/colin/export.png", :at => [box.left, pdf.cursor], :width => box.width, :height => box.width
118
+ end
119
+
120
+ pdf.move_down 10
121
+ # toc
122
+ pdf.bounding_box [box.left, pdf.cursor], :width => box.right, :height => 140 do
123
+ pdf.padded_box 5 do
124
+ create_legend(pdf, :columns => 4, :rows => 5)
125
+ end
126
+ end
127
+
128
+ # footer
129
+ pdf.canvas do
130
+ footer = {
131
+ :top_left => [box.absolute_left - 20, box.absolute_bottom - 15],
132
+ :width => box.right + 40,
133
+ :height => 15
134
+ }
135
+
136
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
137
+ pdf.text '<span class="footer">NB Aquatic Bio-Web</span>'
138
+ end
139
+
140
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
141
+ pdf.text "<span class=\"footer\">Generated on #{Time.now.strftime("%Y/%m/%d")}</span>", :align => :right
142
+ end
143
+ end
144
+ end
145
+ puts 'done'
146
+ end
147
+
148
+ def landscape_pdf
149
+ print 'creating landscape pdf...'
150
+ Prawn::Document.generate '/home/colin/landscape.pdf', :page_layout => :landscape do |pdf|
151
+ box = pdf.bounds
152
+ # html tag definitions
153
+ pdf.tags :h1 => { :font_size => '25', :font_style => :bold }
154
+ # html style definitions
155
+ pdf.styles :footer => { :color => "#999", :font_style => :italic }
156
+
157
+ # map
158
+ pdf.bounding_box [box.left, box.top], :width => box.height, :height => box.height do
159
+ pdf.stroke_bounds
160
+ pdf.image "/home/colin/export.png", :at => [box.left, box.top], :width => box.height, :height => box.height
161
+ end
162
+
163
+ pdf.bounding_box [box.height + 10, box.top], :width => box.right - box.height - 10, :height => box.height do
164
+ # header
165
+ pdf.text "<h1>Title</h1>"
166
+ pdf.move_down 10
167
+
168
+ # toc
169
+ pdf.bounding_box [pdf.bounds.left, pdf.cursor], :width => pdf.bounds.right, :height => pdf.cursor do
170
+ create_legend(pdf, :columns => 1, :rows => 20)
171
+ end
172
+ end
173
+
174
+ # footer
175
+ pdf.canvas do
176
+ footer = {
177
+ :top_left => [box.absolute_left - 20, box.absolute_bottom - 15],
178
+ :width => box.right + 40,
179
+ :height => 15
180
+ }
181
+
182
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
183
+ pdf.text '<span class="footer">NB Aquatic Bio-Web</span>'
184
+ end
185
+
186
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
187
+ pdf.text "<span class=\"footer\">Generated on #{Time.now.strftime("%Y/%m/%d")}</span>", :align => :right
188
+ end
189
+ end
190
+ end
191
+ puts 'done'
192
+ end
193
+
194
+ PerfTools::CpuProfiler.start("/home/colin/pdf_perf") do
195
+ start_time = Time.now
196
+ portrait_pdf
197
+ end_time = Time.now
198
+ puts end_time - start_time
199
+ end
200
+
201
+ #
202
+ #start_time = Time.now
203
+ #landscape_pdf
204
+ #end_time = Time.now
205
+ #puts end_time - start_time
206
+ #end
@@ -0,0 +1,54 @@
1
+ require 'rack'
2
+ #require 'rack/cache'
3
+ require 'activesupport'
4
+ require 'httparty'
5
+
6
+ class PostalCodeLookup
7
+ class ServiceException < Exception; end
8
+
9
+ include HTTParty
10
+ base_uri "geoservices.cgdi.ca"
11
+ format :xml
12
+ default_params :version => '1.0.0',
13
+ :request => 'GetPostalCode',
14
+ :sortarea => 'FSA'
15
+
16
+ def call(env)
17
+ request = Rack::Request.new(env)
18
+
19
+ headers = { "Content-Type" => "text/plain" }
20
+ r = Rack::Response # shorthand
21
+ begin
22
+ response = r.new(200, headers, find(request.params['code']))
23
+ rescue Exception => e
24
+ response = r.new(500, headers, e.message)
25
+ end
26
+ # response.max_age = 1.month
27
+
28
+ response.to_a
29
+ end
30
+
31
+ def find(code)
32
+ result = PostalCodeLookup.get('/cgi-bin/postalcode/postalcode.cgi', {
33
+ :query => { :code => code }
34
+ })
35
+
36
+ if error = result["ServiceExceptionReport"]
37
+ raise ServiceException.new(error['ServiceException'])
38
+ else
39
+ postal_code = result['PostalCodeLookup']['PostalCodeResultSet']['PostalCode']
40
+ location = postal_code["gml:centerOf"]["gml:Point"]
41
+ epsg = location["srsName"].split('#').pop
42
+ lng, lat = location["gml:coordinates"].split(',')
43
+ return {
44
+ :postal_code => postal_code["gml:name"],
45
+ :epsg => epsg,
46
+ :srs_name => location["srsName"],
47
+ :latitude => lat,
48
+ :longitude => lng,
49
+ :placename => postal_code["Placename"],
50
+ :province_or_territory => postal_code["ProvinceOrTerritory"]
51
+ }.to_json
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ require 'arcserver'
2
+ require 'validatable'
3
+
4
+ module PrintMap
5
+ class Job
6
+ include Validatable
7
+ include ArcServer::UrlHelper
8
+ # services validations
9
+ validates_length_of :services, :minimum => 1, :message => 'at least one service needs to be requested'
10
+ validates_true_for :services, :logic => lambda {
11
+ services.all? { |url| map_server?(url) }
12
+ }, :message => 'all services need to be valid MapServer urls'
13
+ # bbox validations
14
+ validates_length_of :bbox, :is => 4, :message => "bad format for 'bbox' - try bbox=xmin,ymin,xmax,ymax"
15
+ validates_true_for :bbox, :logic => lambda {
16
+ env['bbox'].to_s.split(',').all? { |b| b.to_s.match(/^[-|+]?\d*\.?\d*$/) }
17
+ }, :message => 'all bbox values must be valid positive/negative numbers'
18
+
19
+ attr_reader :env
20
+
21
+ def initialize(env = {})
22
+ @env = env
23
+ end
24
+
25
+ def services
26
+ @services ||= env['services'].to_s.split(',')
27
+ end
28
+
29
+ def bbox
30
+ @bbox ||= env['bbox'].to_s.split(',').collect{ |b| b.to_f }
31
+ end
32
+
33
+ def page_layout
34
+ @page_layout ||= (env['page_layout'] || 'portrait').to_sym
35
+ end
36
+
37
+ def page_size
38
+ @page_size ||= env['page_size'] || 'A4'
39
+ end
40
+
41
+ def dpi
42
+ @dpi ||= env['dpi'] || 96
43
+ end
44
+
45
+ def title
46
+ @title = env['title'] || 'Untitled Map'
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ require 'arcserver/map_server'
2
+
3
+ module PrintMap
4
+ class LegendImagesCollector
5
+ def collect(opts = {})
6
+ puts 'creating map server object'
7
+ map_server = ArcServer::MapServer.new(opts[:url])
8
+
9
+ images = []
10
+ puts 'getting legend'
11
+ map_server.get_legend_info.each do |legend_info|
12
+ puts 'processing result'
13
+ legend_classes = legend_info[:legend_groups][0][:legend_classes]
14
+ if legend_classes.length == 1
15
+ img_data = Base64.decode64(legend_classes[0][:symbol_image][:image_data])
16
+ images << {
17
+ :label => legend_info[:name],
18
+ :data => img_data
19
+ }
20
+ else
21
+ legend_classes.each do |legend_class|
22
+ img_data = Base64.decode64(legend_class[:symbol_image][:image_data])
23
+ images << {
24
+ :label => legend_class[:label],
25
+ :data => img_data
26
+ }
27
+ end
28
+ end
29
+ end
30
+
31
+ images
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ require 'httparty'
2
+ require 'RMagick'
3
+
4
+ module PrintMap
5
+ class MapExporter
6
+ def export(opts = {})
7
+ service = opts[:service]
8
+ query = {
9
+ :bbox => opts[:bbox],
10
+ :f => :image,
11
+ :format => :png24,
12
+ :transparent => true,
13
+ :size => opts[:size],
14
+ :dpi => opts[:dpi]
15
+ }
16
+ response = HTTParty.get("#{service}/export", :query => query)
17
+ Magick::Image.from_blob(response.body).first
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,146 @@
1
+ require 'prawn'
2
+ require 'prawn/format'
3
+ require 'prawn/layout'
4
+ require 'open-uri'
5
+
6
+ module PrintMap
7
+ class PdfGenerator
8
+ def generate(opts = {})
9
+ title = opts[:title]
10
+ map_image = opts[:map_image]
11
+ layout = opts[:page_layout]
12
+ legend_images = opts[:legend_images]
13
+
14
+ pdf = Prawn::Document.new(:page_layout => layout) do |pdf|
15
+ # html tag definitions
16
+ pdf.tags :h1 => { :font_size => '25', :font_style => :bold }
17
+ # html style definitions
18
+ pdf.styles :footer => { :color => "#999", :font_style => :italic }
19
+ # generate the pdf
20
+ send("generate_#{layout}_pdf", pdf, title, map_image, legend_images)
21
+ write_footer(pdf)
22
+ end
23
+
24
+ pdf
25
+ end
26
+
27
+ def generate_portrait_pdf(pdf, title, map_image, legend_images)
28
+ box = pdf.bounds
29
+ # header
30
+ pdf.bounding_box [box.left, box.top], :width => box.right, :height => 30 do
31
+ pdf.text "<h1>#{title}</h1>"
32
+ end
33
+
34
+ # map
35
+ pdf.bounding_box [box.left, pdf.cursor], :width => box.right, :height => box.width do
36
+ pdf.stroke_bounds
37
+ pdf.image StringIO.new(map_image.to_blob), :at => [box.left, pdf.cursor], :width => box.width, :height => box.width
38
+ end
39
+
40
+ pdf.move_down 10
41
+ # toc
42
+ pdf.bounding_box [box.left, pdf.cursor], :width => box.right, :height => 140 do
43
+ pdf.padded_box 5 do
44
+ create_legend(pdf, legend_images, :columns => 4, :rows => 5)
45
+ pdf.stroke_bounds
46
+ end
47
+ end
48
+ end
49
+
50
+ def generate_landscape_pdf(pdf, title, image, legend_images)
51
+ box = pdf.bounds
52
+ # map
53
+ pdf.bounding_box [box.left, box.top], :width => box.height, :height => box.height do
54
+ pdf.stroke_bounds
55
+ pdf.image StringIO.new(image.to_blob), :at => [box.left, box.top], :width => box.height, :height => box.height
56
+ end
57
+
58
+
59
+ pdf.bounding_box [box.height + 10, box.top], :width => box.right - box.height - 10, :height => box.height do
60
+ # header
61
+ pdf.text "<h1>#{title}</h1>"
62
+ pdf.move_down 10
63
+
64
+ # toc
65
+ pdf.bounding_box [pdf.bounds.left, pdf.cursor], :width => pdf.bounds.right, :height => pdf.cursor do
66
+ create_legend(pdf, legend_images, :columns => 1, :rows => 20)
67
+ pdf.stroke_bounds
68
+ end
69
+ end
70
+ end
71
+
72
+ def write_footer(pdf)
73
+ box = pdf.bounds
74
+ pdf.canvas do
75
+ footer = {
76
+ :top_left => [box.absolute_left - 20, box.absolute_bottom - 15],
77
+ :width => box.right + 40,
78
+ :height => 15
79
+ }
80
+
81
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
82
+ pdf.text '<span class="footer">NB Aquatic Bio-Web</span>'
83
+ end
84
+
85
+ pdf.bounding_box footer[:top_left], :width => footer[:width], :height => footer[:height] do
86
+ pdf.text "<span class=\"footer\">Generated on #{Time.now.strftime("%Y/%m/%d")}</span>", :align => :right
87
+ end
88
+ end
89
+ end
90
+
91
+ def create_legend(pdf, legend_images, opts = {})
92
+ columns = opts[:columns] || 4
93
+ rows = opts[:rows] || 4
94
+ gutter = opts[:gutter] || 0
95
+
96
+ pdf.define_grid(:columns => columns, :rows => rows, :gutter => gutter)
97
+
98
+ pdf.grid.rows.times do |i|
99
+ pdf.grid.columns.times do |j|
100
+ cell = pdf.grid(i,j)
101
+ pdf.bounding_box cell.top_left, :width => cell.width, :height => cell.height do
102
+ return if legend_images.empty?
103
+ image = legend_images.shift
104
+
105
+ pdf.image StringIO.new(image[:data]), :at => [pdf.bounds.top_left], :fit => [16, 16]
106
+ pdf.text_box image[:label],
107
+ :width => cell.width,
108
+ :height => cell.height,
109
+ :overflow => :ellipses,
110
+ :at => [pdf.bounds.left + 20, pdf.bounds.top - 2]
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ module Prawn
119
+ module Images
120
+ class PNG
121
+ alias_method :prawn_fast_png_old_initialize, :initialize
122
+
123
+ def initialize(data) #:nodoc:
124
+ @prawn_fast_png_data = data
125
+ prawn_fast_png_old_initialize(data)
126
+ end
127
+
128
+ private
129
+ def unfilter_image_data
130
+ img = Magick::Image.from_blob(@prawn_fast_png_data).first
131
+
132
+ # get only one color value per pixel (Intensity) for grayscale+alpha images
133
+ format = color_type == 4 ? 'I' : 'RGB'
134
+
135
+ img_data = img.export_pixels_to_str(0, 0, width, height, format)
136
+ alpha_channel = img.export_pixels_to_str(0, 0, width, height, 'A')
137
+
138
+ img.destroy!
139
+ @prawn_fast_png_data = nil
140
+
141
+ @img_data = Zlib::Deflate.deflate(img_data)
142
+ @alpha_channel = Zlib::Deflate.deflate(alpha_channel)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,71 @@
1
+ require 'prawn'
2
+ require 'RMagick'
3
+ require 'print_map/map_exporter'
4
+ require 'print_map/pdf_generator'
5
+ require 'print_map/legend_images_collector'
6
+
7
+ module PrintMap
8
+ class Worker
9
+ attr_reader :current_job
10
+ attr_reader :map_exporter, :pdf_generator, :legend_images_collector
11
+
12
+ def initialize(config = {})
13
+ @map_exporter = config[:map_exporter] || MapExporter.new
14
+ @pdf_generator = config[:pdf_generator] || PdfGenerator.new
15
+ @legend_images_collector = config[:legend_images_collector] || LegendImagesCollector.new
16
+ end
17
+
18
+ def do_job(job)
19
+ @current_job = job
20
+ puts 'create composite map image'
21
+ map_image = create_composite_map_image
22
+ puts 'collect legend images'
23
+ legend_images = collect_legend_images
24
+ puts 'generate pdf'
25
+ pdf = generate_pdf(map_image, legend_images)
26
+ puts 'cleanup'
27
+ map_image.destroy!
28
+ pdf
29
+ end
30
+
31
+ def create_composite_map_image
32
+ width, height = 540, 510
33
+ base_image = Magick::Image.new(width, height)
34
+ base_image.background_color = 'transparent'
35
+ base_image.format = 'PNG'
36
+
37
+ current_job.services.each do |map_service|
38
+ export_options = {
39
+ :service => map_service,
40
+ :dpi => current_job.dpi,
41
+ :bbox => current_job.bbox.join(','),
42
+ :size => [width, height].join(',')
43
+ }
44
+ map_service_image = map_exporter.export(export_options)
45
+ base_image.composite!(map_service_image, Magick::CenterGravity, Magick::OverCompositeOp)
46
+ map_service_image.destroy!
47
+ end
48
+
49
+ base_image
50
+ end
51
+
52
+ def generate_pdf(map_image, legend_images)
53
+ opts = {
54
+ :title => current_job.title,
55
+ :map_image => map_image,
56
+ :page_layout => current_job.page_layout,
57
+ :legend_images => legend_images
58
+ }
59
+ pdf_generator.generate(opts)
60
+ end
61
+
62
+ def collect_legend_images
63
+ images = []
64
+ current_job.services.each do |url|
65
+ puts 'fetching legends at ' + url
66
+ images << legend_images_collector.collect(:url => url)
67
+ end
68
+ images.flatten
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ require 'print_map/job'
2
+ require 'print_map/worker'
3
+
4
+ class PrintMapService
5
+ attr_reader :worker, :create_job
6
+
7
+ def initialize(opts = {})
8
+ @worker = opts[:worker] ||= PrintMap::Worker.new
9
+ @create_job = opts[:create_job] ||= proc { |opts| PrintMap::Job.new(opts) }
10
+ end
11
+
12
+ def call(env)
13
+ puts 'calling print map'
14
+ job = create_job.call(Rack::Request.new(env).params)
15
+ if job.valid?
16
+ puts 'valid job'
17
+ pdf = worker.do_job(job)
18
+ puts 'rendering to client'
19
+ # now stream the pdf to the client
20
+ [
21
+ 200, {
22
+ "Content-Type" => "application/pdf",
23
+ "Content-Disposition" => "attachment; filename=\"#{job.title}.pdf\""
24
+ },
25
+ pdf.render
26
+ ]
27
+ else
28
+ create_invalid_job_response(job)
29
+ end
30
+
31
+ rescue Exception => e
32
+ create_error_response(e)
33
+ end
34
+
35
+ def create_invalid_job_response(job)
36
+ body = "Print map failed because of the following errors:\n#{job.errors.full_messages.join("\n")}"
37
+ puts 'failed to generate print map:'
38
+ puts body
39
+ [500, { "Content-Type" => "text/plain" }, body]
40
+ end
41
+
42
+ def create_error_response(e)
43
+ body = "An internal error occurred:\n#{e.message}"
44
+ puts 'failed to generate print map:'
45
+ puts body
46
+ [500, { "Content-Type" => "text/plain" }, body]
47
+ end
48
+ end
data/lib/rest_proxy.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'rack'
2
+ require 'httparty'
3
+
4
+ class RestProxy
5
+ def call(env)
6
+ request = Rack::Request.new(env)
7
+ url = request.params.delete('url')
8
+ call_proxy(url)
9
+ end
10
+
11
+ def call_proxy(url)
12
+ response = HTTParty.get(url, :query => { :f => :json })
13
+ [response.code, response.headers, response.body]
14
+ end
15
+ end