kronk 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/kronk/diff.rb CHANGED
@@ -100,7 +100,7 @@ class Kronk
100
100
  "#{pad}#{key.inspect} => #{subdata}"
101
101
  end
102
102
 
103
- output << data_values.sort.join(",\n") << "\n"
103
+ output << data_values.sort.join(",\n") << "\n" unless data_values.empty?
104
104
  output << "#{" " * indent}}"
105
105
 
106
106
  when Array
@@ -112,7 +112,7 @@ class Kronk
112
112
  "#{pad}#{ordered_data_string value, struct_only, indent + 1}"
113
113
  end
114
114
 
115
- output << data_values.join(",\n") << "\n"
115
+ output << data_values.join(",\n") << "\n" unless data_values.empty?
116
116
  output << "#{" " * indent}]"
117
117
 
118
118
  else
data/lib/kronk/request.rb CHANGED
@@ -5,6 +5,7 @@ class Kronk
5
5
 
6
6
  class Request
7
7
 
8
+
8
9
  class NotFoundError < Exception; end
9
10
 
10
11
  ##
@@ -17,7 +18,10 @@ class Kronk
17
18
  rdir = options[:follow_redirects]
18
19
  rdir = rdir - 1 if Integer === rdir && rdir > 0
19
20
 
20
- retrieve_uri resp['Location'], options.merge(:follow_redirects => rdir)
21
+ options = options.merge :follow_redirects => rdir,
22
+ :http_method => :get
23
+
24
+ retrieve_uri resp['Location'], options
21
25
  end
22
26
 
23
27
 
@@ -34,16 +38,22 @@ class Kronk
34
38
  # Returns the value from a url, file, or cache as a String.
35
39
  # Options supported are:
36
40
  # :data:: Hash/String - the data to pass to the http request
41
+ # :query:: Hash/String - the data to append to the http request path
37
42
  # :follow_redirects:: Integer/Bool - number of times to follow redirects
43
+ # :user_agent:: String - user agent string or alias; defaults to 'kronk'
44
+ # :auth:: Hash - must contain :username and :password; defaults to nil
38
45
  # :headers:: Hash - extra headers to pass to the request
39
46
  # :http_method:: Symbol - the http method to use; defaults to :get
47
+ # :proxy:: Hash/String - http proxy to use; defaults to nil
40
48
 
41
49
  def self.retrieve query, options={}
42
50
  resp =
43
- if !local?(query)
44
- retrieve_uri query, options
45
- else
51
+ if IO === query || StringIO === query
52
+ retrieve_io query, options
53
+ elsif local?(query)
46
54
  retrieve_file query, options
55
+ else
56
+ retrieve_uri query, options
47
57
  end
48
58
 
49
59
  begin
@@ -72,7 +82,7 @@ class Kronk
72
82
  # Read http response from a file and return a HTTPResponse instance.
73
83
 
74
84
  def self.retrieve_file path, options={}
75
- Kronk.verbose "\nReading file:\n#{path}\n"
85
+ Kronk.verbose "Reading file: #{path}\n"
76
86
 
77
87
  options = options.dup
78
88
 
@@ -97,20 +107,47 @@ class Kronk
97
107
  end
98
108
 
99
109
 
110
+ ##
111
+ # Read the http response from an IO instance and return a HTTPResponse.
112
+
113
+ def self.retrieve_io io, options={}
114
+ Kronk.verbose "Reading IO..."
115
+
116
+ options = options.dup
117
+
118
+ resp = nil
119
+
120
+ begin
121
+ resp = Response.read_new io
122
+
123
+ rescue Net::HTTPBadResponse
124
+ io.rewind
125
+ resp = HeadlessResponse.new io.read
126
+ end
127
+
128
+ resp = follow_redirect resp, options if
129
+ follow_redirect? resp, options[:follow_redirects]
130
+
131
+ resp
132
+ end
133
+
134
+
100
135
  ##
101
136
  # Make an http request to the given uri and return a HTTPResponse instance.
102
137
  # Supports the following options:
103
138
  # :data:: Hash/String - the data to pass to the http request
139
+ # :query:: Hash/String - the data to append to the http request path
104
140
  # :follow_redirects:: Integer/Bool - number of times to follow redirects
141
+ # :user_agent:: String - user agent string or alias; defaults to 'kronk'
142
+ # :auth:: Hash - must contain :username and :password; defaults to nil
105
143
  # :headers:: Hash - extra headers to pass to the request
106
144
  # :http_method:: Symbol - the http method to use; defaults to :get
145
+ # :proxy:: Hash/String - http proxy to use; defaults to nil
107
146
  #
108
147
  # Note: if no http method is specified and data is given, will default
109
148
  # to using a post request.
110
149
 
111
150
  def self.retrieve_uri uri, options={}
112
- Kronk.verbose "\nRetrieving URL: #{uri}#{options[:uri_suffix]}\n"
113
-
114
151
  options = options.dup
115
152
  http_method = options.delete(:http_method)
116
153
  http_method ||= options[:data] ? :post : :get
@@ -127,32 +164,70 @@ class Kronk
127
164
  ##
128
165
  # Make an http request to the given uri and return a HTTPResponse instance.
129
166
  # Supports the following options:
130
- # :data:: Hash/String - the data to pass to the http request
131
- # :follow_redirects:: Integer/Bool - number of times to follow redirects
167
+ # :data:: Hash/String - the data to pass to the http request body
168
+ # :query:: Hash/String - the data to append to the http request path
169
+ # :user_agent:: String - user agent string or alias; defaults to 'kronk'
170
+ # :auth:: Hash - must contain :username and :password; defaults to nil
132
171
  # :headers:: Hash - extra headers to pass to the request
133
172
  # :http_method:: Symbol - the http method to use; defaults to :get
173
+ # :proxy:: Hash/String - http proxy to use; defaults to nil
134
174
 
135
175
  def self.call http_method, uri, options={}
136
176
  suffix = options.delete :uri_suffix
137
177
 
138
- uri = "#{uri}#{suffix}" if suffix
139
- uri = URI.parse uri unless URI === uri
178
+ uri = "#{uri}#{suffix}" if suffix
179
+ uri = URI.parse uri unless URI === uri
180
+
181
+ if options[:query]
182
+ query = build_query options[:query]
183
+ uri.query = [uri.query, query].compact.join "&"
184
+ end
140
185
 
141
186
  data = options[:data]
142
- data &&= Hash === data ? build_query(data) : data.to_s
187
+ data &&= build_query data
188
+
189
+ options[:headers] ||= Hash.new
190
+ options[:headers]['User-Agent'] ||= get_user_agent options[:user_agent]
191
+
192
+ unless options[:headers]['Cookie'] || !use_cookies?(options)
193
+ cookie = Kronk.cookie_jar.get_cookie_header uri.to_s
194
+ options[:headers]['Cookie'] = cookie unless cookie.empty?
195
+ end
143
196
 
144
197
  socket = socket_io = nil
145
198
 
146
- resp = Net::HTTP.start uri.host, uri.port do |http|
199
+ proxy_addr, proxy_opts =
200
+ if Hash === options[:proxy]
201
+ [options[:proxy][:address], options[:proxy]]
202
+ else
203
+ [options[:proxy], {}]
204
+ end
205
+
206
+ http_class = proxy proxy_addr, proxy_opts
207
+
208
+ req = http_class.new uri.host, uri.port
209
+ req.use_ssl = true if uri.scheme =~ /^https$/
210
+
211
+ resp = req.start do |http|
147
212
  socket = http.instance_variable_get "@socket"
148
213
  socket.debug_output = socket_io = StringIO.new
149
214
 
150
- http.send_request http_method.to_s.upcase,
151
- uri.request_uri,
152
- data,
153
- options[:headers]
215
+ req = VanillaRequest.new http_method.to_s.upcase,
216
+ uri.request_uri, options[:headers]
217
+
218
+ if options[:auth] && options[:auth][:username]
219
+ req.basic_auth options[:auth][:username],
220
+ options[:auth][:password]
221
+ end
222
+
223
+ Kronk.verbose "Retrieving URL: #{uri}\n"
224
+
225
+ http.request req, data
154
226
  end
155
227
 
228
+ Kronk.cookie_jar.set_cookies_from_headers uri.to_s, resp.to_hash if
229
+ use_cookies? options
230
+
156
231
  resp.extend Response::Helpers
157
232
 
158
233
  r_req, r_resp, r_bytes = Response.read_raw_from socket_io
@@ -162,13 +237,49 @@ class Kronk
162
237
  end
163
238
 
164
239
 
240
+ ##
241
+ # Checks if cookies should be used and set.
242
+
243
+ def self.use_cookies? options
244
+ return !options[:no_cookies] if options.has_key? :no_cookies
245
+ Kronk.config[:use_cookies]
246
+ end
247
+
248
+
249
+ ##
250
+ # Gets the user agent to use for the request.
251
+
252
+ def self.get_user_agent agent
253
+ agent && Kronk.config[:user_agents][agent.to_s] || agent ||
254
+ Kronk.config[:user_agents]['kronk']
255
+ end
256
+
257
+
258
+ ##
259
+ # Return proxy http class.
260
+ # The proxy_opts arg can be a uri String or a Hash with the :address key
261
+ # and optional :username and :password keys.
262
+
263
+ def self.proxy addr, proxy_opts={}
264
+ return Net::HTTP unless addr
265
+
266
+ host, port = addr.split ":"
267
+ port ||= proxy_opts[:port] || 8080
268
+
269
+ user = proxy_opts[:username]
270
+ pass = proxy_opts[:password]
271
+
272
+ Kronk.verbose "Using proxy #{addr}\n" if host
273
+
274
+ Net::HTTP::Proxy host, port, user, pass
275
+ end
276
+
277
+
165
278
  ##
166
279
  # Creates a query string from data.
167
280
 
168
281
  def self.build_query data, param=nil
169
- raise ArgumentError,
170
- "Can't convert #{data.class} to query without a param name" unless
171
- Hash === data || param
282
+ return data.to_s unless param || Hash === data
172
283
 
173
284
  case data
174
285
  when Array
@@ -191,5 +302,20 @@ class Kronk
191
302
  "#{param}=#{data}"
192
303
  end
193
304
  end
305
+
306
+
307
+ ##
308
+ # Allow any http method to be sent
309
+
310
+ class VanillaRequest
311
+ def self.new method, path, initheader=nil
312
+ klass = Class.new Net::HTTPRequest
313
+ klass.const_set "METHOD", method.to_s.upcase
314
+ klass.const_set "REQUEST_HAS_BODY", true
315
+ klass.const_set "RESPONSE_HAS_BODY", true
316
+
317
+ klass.new path, initheader
318
+ end
319
+ end
194
320
  end
195
321
  end
@@ -1,7 +1,7 @@
1
1
  class Kronk
2
2
 
3
3
  ##
4
- # Wrapper class for Nokogiri parser.
4
+ # Rails-like XML parser.
5
5
 
6
6
  class XMLParser
7
7
 
@@ -0,0 +1,28 @@
1
+ HTTP/1.1 200 OK
2
+ Server: nginx/0.6.39
3
+ Date: Fri, 03 Dec 2010 21:49:00 GMT
4
+ Content-Type: application/json; charset=utf-8
5
+ Connection: keep-alive
6
+ Keep-Alive: timeout=20
7
+ Status: 200 OK
8
+ ETag: "mock_etag"
9
+ X-Runtime: 45
10
+ Cache-Control: private, max-age=0, must-revalidate
11
+
12
+ {"original_request":{"id":"1234"},"request_id":"mock_rid",
13
+ "business":{"improvable":true,"longitude":-85.759586,"headings":["Pottery"],
14
+ "website":"http://example.com",
15
+ "listing_type":"free",
16
+ "description":{"general_info":"<p>A Paint Your Own Pottery Studios..</p>",
17
+ "payment_text":"DISCOVER, AMEX, VISA, MASTERCARD",
18
+ "slogan":"<p>Pottery YOU dress up</p>",
19
+ "additional_urls":[{"destination":"http://example.com",
20
+ "url_click":"http://example.com"}],
21
+ "op_hours":"Fri 1pm-7pm, Sat 10am-6pm, Sun 1pm-4pm, Appointments Available"},
22
+ "listing_id":"1234","phone":"6168055326",
23
+ "address":"3845 Rivertown Pkwy SW Ste 500",
24
+ "mappable":true,"rating_count":0,"rateable":true,
25
+ "impression_id":"mock_iid","zip":"49418","red_listing":false,"omit_phone":false,
26
+ "state":"MI","distance":0.0,"year_established":"1996","city":"Grandville",
27
+ "name":"Naked Plates","latitude":42.882561,"omit_address":false,"id":"1234",
28
+ "has_detail_page":true}}
@@ -0,0 +1,97 @@
1
+ HTTP/1.1 200 OK
2
+ Server: nginx/0.6.39
3
+ Date: Fri, 03 Dec 2010 21:49:00 GMT
4
+ Content-Type: application/x-plist; charset=utf-8
5
+ Connection: keep-alive
6
+ Keep-Alive: timeout=20
7
+ Status: 200 OK
8
+ ETag: "mock_etag"
9
+ X-Runtime: 45
10
+ Cache-Control: private, max-age=0, must-revalidate
11
+
12
+ <?xml version="1.0" encoding="UTF-8"?>
13
+ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
14
+ <plist version="1.0">
15
+ <dict>
16
+ <key>business</key>
17
+ <dict>
18
+ <key>address</key>
19
+ <string>3845 Rivertown Pkwy SW Ste 500</string>
20
+ <key>city</key>
21
+ <string>Grandville</string>
22
+ <key>description</key>
23
+ <dict>
24
+ <key>additional_urls</key>
25
+ <array>
26
+ <dict>
27
+ <key>destination</key>
28
+ <string>http://example.com</string>
29
+ <key>url_click</key>
30
+ <string>http://example.com</string>
31
+ </dict>
32
+ </array>
33
+ <key>general_info</key>
34
+ <string>&lt;p&gt;A Paint Your Own Pottery Studios..&lt;/p&gt;</string>
35
+ <key>op_hours</key>
36
+ <string>Fri 1pm-7pm, Sat 10am-6pm, Sun 1pm-4pm, Appointments Available</string>
37
+ <key>payment_text</key>
38
+ <string>DISCOVER, AMEX, VISA, MASTERCARD</string>
39
+ <key>slogan</key>
40
+ <string>&lt;p&gt;Pottery YOU dress up&lt;/p&gt;</string>
41
+ </dict>
42
+ <key>distance</key>
43
+ <real>0.0</real>
44
+ <key>has_detail_page</key>
45
+ <true/>
46
+ <key>headings</key>
47
+ <array>
48
+ <string>Pottery</string>
49
+ </array>
50
+ <key>id</key>
51
+ <string>1234</string>
52
+ <key>impression_id</key>
53
+ <string>mock_iid</string>
54
+ <key>improvable</key>
55
+ <true/>
56
+ <key>latitude</key>
57
+ <real>42.882561</real>
58
+ <key>listing_id</key>
59
+ <string>1234</string>
60
+ <key>listing_type</key>
61
+ <string>free</string>
62
+ <key>longitude</key>
63
+ <real>-85.759586</real>
64
+ <key>mappable</key>
65
+ <true/>
66
+ <key>name</key>
67
+ <string>Naked Plates</string>
68
+ <key>omit_address</key>
69
+ <false/>
70
+ <key>omit_phone</key>
71
+ <false/>
72
+ <key>phone</key>
73
+ <string>6168055326</string>
74
+ <key>rateable</key>
75
+ <true/>
76
+ <key>rating_count</key>
77
+ <integer>0</integer>
78
+ <key>red_listing</key>
79
+ <false/>
80
+ <key>state</key>
81
+ <string>MI</string>
82
+ <key>website</key>
83
+ <string>http://example.com</string>
84
+ <key>year_established</key>
85
+ <string>1996</string>
86
+ <key>zip</key>
87
+ <string>49418</string>
88
+ </dict>
89
+ <key>original_request</key>
90
+ <dict>
91
+ <key>id</key>
92
+ <string>1234</string>
93
+ </dict>
94
+ <key>request_id</key>
95
+ <string>mock_rid</string>
96
+ </dict>
97
+ </plist>
@@ -0,0 +1,23 @@
1
+ HTTP/1.1 200 OK
2
+ Date: Fri, 26 Nov 2010 16:16:08 GMT
3
+ Expires: -1
4
+ Cache-Control: private, max-age=0
5
+ Content-Type: text/html; charset=ISO-8859-1
6
+ Set-Cookie: PREF=ID=99d644506f26d85e:FF=0:TM=1290788168:LM=1290788168:S=VSMemgJxlmlToFA3; expires=Sun, 25-Nov-2012 16:16:08 GMT; path=/; domain=.google.com
7
+ Set-Cookie: NID=41=CcmNDE4SfDu5cdTOYVkrCVjlrGO-oVbdo1awh_p8auk2gI4uaX1vNznO0QN8nZH4Mh9WprRy3yI2yd_Fr1WaXVru6Xq3adlSLGUTIRW8SzX58An2nH3D2PhAY5JfcJrl; expires=Sat, 28-May-2011 16:16:08 GMT; path=/; domain=.google.com; HttpOnly
8
+ Server: gws
9
+ X-XSS-Protection: 1; mode=block
10
+ Transfer-Encoding: chunked
11
+
12
+ <!doctype html><html><head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"><title>Google</title><script>window.google={kEI:"SN3vTLbkBJLSpASAoaHKCg",kEXPI:"27820",kCSI:{e:"27820",ei:"SN3vTLbkBJLSpASAoaHKCg",expi:"27820"},ml:function(){},kHL:"en",time:function(){return(new Date).getTime()},log:function(b,d,
13
+ c){var a=new Image,e=google,g=e.lc,f=e.li;a.onerror=(a.onload=(a.onabort=function(){delete g[f]}));g[f]=a;c=c||"/gen_204?atyp=i&ct="+b+"&cad="+d+"&zx="+google.time();a.src=c;e.li=f+1},lc:[],li:0,Toolbelt:{}};
14
+ window.google.sn="webhp";var i=window.google.timers={};window.google.startTick=function(a,b){i[a]={t:{start:(new Date).getTime()},bfr:!(!b)}};window.google.tick=function(a,b,c){if(!i[a])google.startTick(a);i[a].t[b]=c||(new Date).getTime()};google.startTick("load",true);try{}catch(v){}
15
+ window.google.jsrt_kill=1;
16
+ var _gjwl=location;function _gjuc(){var e=_gjwl.href.indexOf("#");if(e>=0){var a=_gjwl.href.substring(e);if(a.indexOf("&q=")>0||a.indexOf("#q=")>=0){a=a.substring(1);if(a.indexOf("#")==-1){for(var c=0;c<a.length;){var d=c;if(a.charAt(d)=="&")++d;var b=a.indexOf("&",d);if(b==-1)b=a.length;var f=a.substring(d,b);if(f.indexOf("fp=")==0){a=a.substring(0,c)+a.substring(b,a.length);b=c}else if(f=="cad=h")return 0;c=b}_gjwl.href="/search?"+a+"&cad=h";return 1}}}return 0}function _gjp(){!(window._gjwl.hash&&
17
+ window._gjuc())&&setTimeout(_gjp,500)};
18
+ window._gjp && _gjp()</script><style id=gstyle>body{margin:0}#gog{padding:3px 8px 0}td{line-height:.8em}.gac_m td{line-height:17px}form{margin-bottom:20px}body,td,a,p,.h{font-family:arial,sans-serif}.h{color:#36c;font-size:20px}.q{color:#00c}.ts td{padding:0}.ts{border-collapse:collapse}em{font-weight:bold;font-style:normal}.lst{width:496px}.tiah{width:458px}input{font-family:inherit}a.gb1,a.gb2,a.gb3,a.gb4{color:#11c !important}#gbar,#guser{font-size:13px;padding-top:1px !important}#gbar{height:22px}#guser{padding-bottom:7px !important;text-align:right}.gbh,.gbd{border-top:1px solid #c9d7f1;font-size:1px}.gbh{height:0;position:absolute;top:24px;width:100%}@media all{.gb1{height:22px;margin-right:.5em;vertical-align:top}#gbar{float:left}}a.gb1,a.gb4{color:#00c !important}body{background:#fff;color:black}input{-moz-box-sizing:content-box}a{color:#11c;text-decoration:none}a:hover,a:active{text-decoration:underline}.fl a{color:#4272db}a:visited{color:#551a8b}a.gb1,a.gb4{text-decoration:underline}a.gb3:hover{text-decoration:none}#ghead a.gb2:hover{color:#fff!important}.ds{display:-moz-inline-box}.ds{border-bottom:solid 1px #e7e7e7;border-right:solid 1px #e7e7e7;display:inline-block;margin:3px 0 4px;margin-left:4px}.sblc{padding-top:5px}.sblc a{display:block;margin:2px 0;margin-left:13px;font-size:11px;}.lsbb{background:#eee;border:solid 1px;border-color:#ccc #999 #999 #ccc;height:30px;display:block}.lsb{background:url(/images/srpr/nav_logo27.png) bottom;font:15px arial,sans-serif;border:none;color:#000;cursor:pointer;height:30px;margin:0;outline:0;vertical-align:top}.lsb:active{background:#ccc}.lst:focus{outline:none}.ftl,#fll a{margin:0 12px}#addlang a{padding:0 3px}.gac_v div{display:none}.gac_v .gac_v2,.gac_bt{display:block!important}</style><script>google.y={};google.x=function(e,g){google.y[e.id]=[e,g];return false};</script></head><body bgcolor=#ffffff text=#000000 link=#0000cc vlink=#551a8b alink=#ff0000 onload="document.f.q.focus();if(document.images)new Image().src='/images/srpr/nav_logo27.png'" ><textarea id=csi style=display:none></textarea><iframe name=wgjf style=display:none></iframe><div id=ghead><div id=gbar><nobr><b class=gb1>Web</b> <a onclick=gbar.qs(this) href="http://www.google.com/imghp?hl=en&tab=wi" class=gb1>Images</a> <a onclick=gbar.qs(this) href="http://video.google.com/?hl=en&tab=wv" class=gb1>Videos</a> <a onclick=gbar.qs(this) href="http://maps.google.com/maps?hl=en&tab=wl" class=gb1>Maps</a> <a onclick=gbar.qs(this) href="http://news.google.com/nwshp?hl=en&tab=wn" class=gb1>News</a> <a onclick=gbar.qs(this) href="http://www.google.com/prdhp?hl=en&tab=wf" class=gb1>Shopping</a> <a href="http://mail.google.com/mail/?hl=en&tab=wm" class=gb1>Gmail</a> <a href="http://www.google.com/intl/en/options/" class=gb1 style="text-decoration:none"><u>more</u> &raquo;</a></nobr></div><div id=guser width=100%><nobr><span id=gbn class=gbi></span><span id=gbf class=gbf></span><span id=gbe><a href="/url?sa=p&pref=ig&pval=3&q=http://www.google.com/ig%3Fhl%3Den%26source%3Diglk&usg=AFQjCNFA18XPfgb7dKnXfKz7x7g1GDH1tg" class=gb4>iGoogle</a> | </span><a href="/preferences?hl=en" class=gb4>Settings</a> | <a href="https://www.google.com/accounts/Login?hl=en&continue=http://www.google.com/" class=gb4>Sign in</a></nobr></div><div class=gbh style=left:0></div><div class=gbh style=right:0></div></div> <center><br clear=all id=lgpd><div id=lga><img alt="Google" height=95 src="/intl/en_ALL/images/srpr/logo1w.png" width=275 id=logo style="padding:28px 0 14px" onload="window.lol&&lol()"><br><br></div><form action="/search" name=f><table cellpadding=0 cellspacing=0><tr valign=top><td width=25%>&nbsp;</td><td align=center nowrap><input name=hl type=hidden value=en><input name=source type=hidden value=hp><input type=hidden name=ie value="ISO-8859-1"><div class=ds style="height:32px;margin:4px 0"><input autocomplete="off" maxlength=2048 name=q class="lst" title="Google Search" value="" size=57 style="background:#fff;border:1px solid #ccc;border-bottom-color:#999;border-right-color:#999;color:#000;font:18px arial,sans-serif bold;height:25px;margin:0;padding:5px 8px 0 6px;vertical-align:top"></div><br style="line-height:0"><span class=ds ><span class=lsbb><input name=btnG type=submit value="Google Search" class=lsb></span></span><span class=ds><span class=lsbb><input name=btnI type=submit value="I&#39;m Feeling Lucky" class=lsb></span></span></td><td nowrap width=25% align=left class=sblc><a href="/advanced_search?hl=en">Advanced Search</a><a href="/language_tools?hl=en">Language Tools</a></td></tr></table></form><div style="font-size:83%;min-height:3.5em"><br></div><div id=res></div><span id=footer><center id=fctr><div style="font-size:10pt"><div id=fll style="margin:19px auto 19px auto;text-align:center"><a href="/intl/en/ads/">Advertising&nbsp;Programs</a><a href="/services/">Business Solutions</a><a href="/intl/en/about.html">About Google</a></div></div><p style="color:#767676;font-size:8pt">&copy; 2010 - <a href="/intl/en/privacy.html">Privacy</a></p></center></span> <div id=xjsd></div><div id=xjsi><script>if(google.y)google.y.first=[];google.dlj=function(b){window.setTimeout(function(){var a=document.createElement("script");a.src=b;document.getElementById("xjsd").appendChild(a)},0)};
19
+ if(google.y)google.y.first=[];if(!google.xjs){google.dstr=[];google.rein=[];if (google.timers && google.timers.load.t) {google.timers.load.t.xjsls=new Date().getTime();}google.dlj('/extern_js/f/CgJlbhICdXMgACswRTgALCswWjgALCswDjgALCswFzgALCswJzgALCswPDgALCswCjgAQC8sKzAWOAAsKzAlOM-IASwrMEA4ACwrMEE4ACwrME04ACwrMFQ4ACwrMGk4ACwrMBg4ACwrMCY4ACyAAiiQAiU/btDx_PEMAx4.js');google.xjs=1}google.neegg=1;google.mc = [];google.mc = google.mc.concat([[14,{}],[64,{}],[105,{}],[22,{"m_error":"\u003Cfont color=red\u003EError:\u003C/font\u003E The server could not complete your request. Try again in 30 seconds.","m_tip":"Click for more information"}],[84,{}]]);google.y.first.push(function(){var form=document.f||document.f||document.gs;google.ac.i(form,form.q,'','','',{a:1,l:1,o:1,sw:1});google.med&&google.med('init');google.History&&google.History.initialize('/')});if(google.j&&google.j.en&&google.j.xi){window.setTimeout(google.j.xi,0);google.fade=null;}</script></div><script>(function(){
20
+ var b,d,e,f;function g(a,c){if(a.removeEventListener){a.removeEventListener("load",c,false);a.removeEventListener("error",c,false)}else{a.detachEvent("onload",c);a.detachEvent("onerror",c)}}function h(a){f=(new Date).getTime();++d;a=a||window.event;var c=a.target||a.srcElement;g(c,h)}var i=document.getElementsByTagName("img");b=i.length;d=0;for(var j=0,k;j<b;++j){k=i[j];if(k.complete||typeof k.src!="string"||!k.src)++d;else if(k.addEventListener){k.addEventListener("load",h,false);k.addEventListener("error",
21
+ h,false)}else{k.attachEvent("onload",h);k.attachEvent("onerror",h)}}e=b-d;function l(){if(!google.timers.load.t)return;google.timers.load.t.ol=(new Date).getTime();google.timers.load.t.iml=f;google.kCSI.imc=d;google.kCSI.imn=b;google.kCSI.imp=e;google.timers.load.t.xjs&&google.report&&google.report(google.timers.load,google.kCSI)}if(window.addEventListener)window.addEventListener("load",l,false);else if(window.attachEvent)window.attachEvent("onload",l);google.timers.load.t.prt=(f=(new Date).getTime());
22
+ })();
23
+ </script>
@@ -0,0 +1,58 @@
1
+ HTTP/1.1 200 OK
2
+ Server: nginx/0.6.39
3
+ Date: Fri, 03 Dec 2010 21:49:00 GMT
4
+ Content-Type: application/xml; charset=utf-8
5
+ Connection: keep-alive
6
+ Keep-Alive: timeout=20
7
+ Status: 200 OK
8
+ ETag: "mock_etag"
9
+ X-Runtime: 45
10
+ Cache-Control: private, max-age=0, must-revalidate
11
+
12
+ <?xml version="1.0" encoding="UTF-8"?>
13
+ <business_result>
14
+ <original_request>
15
+ <id>1234</id>
16
+ </original_request>
17
+ <request_id>mock_rid</request_id>
18
+ <business id="1234">
19
+ <description>
20
+ <general_info>&lt;p&gt;A Paint Your Own Pottery Studios..&lt;/p&gt;</general_info>
21
+ <payment_text>DISCOVER, AMEX, VISA, MASTERCARD</payment_text>
22
+ <slogan>&lt;p&gt;Pottery YOU dress up&lt;/p&gt;</slogan>
23
+ <additional_urls>
24
+ <additional_url>
25
+ <destination>http://example.com</destination>
26
+ <url_click>http://example.com</url_click>
27
+ </additional_url>
28
+ </additional_urls>
29
+ <op_hours>Fri 1pm-7pm, Sat 10am-6pm, Sun 1pm-4pm, Appointments Available</op_hours>
30
+ </description>
31
+ <improvable type="boolean">true</improvable>
32
+ <longitude type="float">-85.759586</longitude>
33
+ <headings>
34
+ <heading>Pottery</heading>
35
+ </headings>
36
+ <website>http://example.com</website>
37
+ <listing_type>free</listing_type>
38
+ <listing_id>1234</listing_id>
39
+ <phone>6168055326</phone>
40
+ <address>3845 Rivertown Pkwy SW Ste 500</address>
41
+ <mappable type="boolean">true</mappable>
42
+ <rating_count type="integer">0</rating_count>
43
+ <rateable type="boolean">true</rateable>
44
+ <impression_id>mock_iid</impression_id>
45
+ <zip>49418</zip>
46
+ <red_listing type="boolean">false</red_listing>
47
+ <omit_phone type="boolean">false</omit_phone>
48
+ <state>MI</state>
49
+ <distance type="float">0.0</distance>
50
+ <year_established>1996</year_established>
51
+ <city>Grandville</city>
52
+ <name>Naked Plates</name>
53
+ <latitude type="float">42.882561</latitude>
54
+ <omit_address type="boolean">false</omit_address>
55
+ <id>1234</id>
56
+ <has_detail_page type="boolean">true</has_detail_page>
57
+ </business>
58
+ </business_result>