kronk 1.0.3 → 1.1.0

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.
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>