manticore 0.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,391 @@
1
+ require 'thread'
2
+
3
+ module Manticore
4
+ # General Timeout exception thrown for various Manticore timeouts
5
+ class Timeout < ManticoreException; end
6
+
7
+ # Core Manticore client, with a backing {http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html PoolingHttpClientConnectionManager}
8
+ class Client
9
+ include_package "org.apache.http.client.methods"
10
+ include_package "org.apache.http.client.entity"
11
+ include_package "org.apache.http.client.config"
12
+ include_package "org.apache.http.config"
13
+ include_package "org.apache.http.impl"
14
+ include_package "org.apache.http.impl.client"
15
+ include_package "org.apache.http.impl.conn"
16
+ include_package "org.apache.http.entity"
17
+ include_package "org.apache.http.message"
18
+ include_package "org.apache.http.params"
19
+ include_package "org.apache.http.protocol"
20
+ include_package "java.util.concurrent"
21
+ java_import 'java.util.concurrent.TimeUnit'
22
+ java_import 'java.util.concurrent.CountDownLatch'
23
+ java_import 'java.util.concurrent.LinkedBlockingQueue'
24
+
25
+ # The default maximum pool size for requests
26
+ DEFAULT_MAX_POOL_SIZE = 50
27
+
28
+ # The default maximum number of threads per route that will be permitted
29
+ DEFAULT_MAX_PER_ROUTE = 2
30
+
31
+ DEFAULT_REQUEST_TIMEOUT = 60
32
+ DEFAULT_SOCKET_TIMEOUT = 10
33
+ DEFAULT_CONNECT_TIMEOUT = 10
34
+ DEFAULT_MAX_REDIRECTS = 5
35
+ DEFAULT_EXPECT_CONTINUE = false
36
+ DEFAULT_STALE_CHECK = false
37
+
38
+ # Create a new HTTP client with a backing request pool. if you pass a block to the initializer, the underlying
39
+ # {http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClientBuilder.html HttpClientBuilder}
40
+ # and {http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html RequestConfig.Builder}
41
+ # will be yielded so that you can operate on them directly.
42
+ #
43
+ # @see http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClientBuilder.html HttpClientBuilder
44
+ # @see http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html RequestConfig.Builder
45
+ # @example Simple instantiation and usage
46
+ # client = Manticore::Client.new
47
+ # client.get("http://www.google.com")
48
+ #
49
+ # @example Instantiation with a block
50
+ # client = Manticore::Client.new(socket_timeout: 5) do |http_client_builder, request_builder|
51
+ # http_client_builder.disable_redirect_handling
52
+ # end
53
+ #
54
+ # @param options [Hash] Client pool options
55
+ # @option options [String] user_agent The user agent used in requests.
56
+ # @option options [Integer] pool_max (50) The maximum number of active connections in the pool
57
+ # @option options [integer] pool_max_per_route (2) Sets the maximum number of active connections for a given target endpoint
58
+ # @option options [boolean] cookies (true) enable or disable automatic cookie management between requests
59
+ # @option options [boolean] compression (true) enable or disable transparent gzip/deflate support
60
+ # @option options [integer] request_timeout (60) Sets the timeout for requests. Raises Manticore::Timeout on failure.
61
+ # @option options [integer] connect_timeout (10) Sets the timeout for connections. Raises Manticore::Timeout on failure.
62
+ # @option options [integer] socket_timeout (10) Sets SO_TIMEOUT for open connections. A value of 0 is an infinite timeout. Raises Manticore::Timeout on failure.
63
+ # @option options [integer] request_timeout (60) Sets the timeout for a given request. Raises Manticore::Timeout on failure.
64
+ # @option options [integer] max_redirects (5) Sets the maximum number of redirects to follow.
65
+ # @option options [boolean] expect_continue (false) Enable support for HTTP 100
66
+ # @option options [boolean] stale_check (false) Enable support for stale connection checking. Adds overhead.
67
+ def initialize(options = {})
68
+ builder = client_builder
69
+ builder.set_user_agent options.fetch(:user_agent, "Manticore #{VERSION}")
70
+ builder.disable_cookie_management unless options.fetch(:cookies, false)
71
+ builder.disable_content_compression if options.fetch(:compression, true) == false
72
+
73
+ # This should make it easier to reuse connections
74
+ builder.disable_connection_state
75
+ builder.set_connection_reuse_strategy DefaultConnectionReuseStrategy.new
76
+
77
+ # socket_config = SocketConfig.custom.set_tcp_no_delay(true).build
78
+ builder.set_connection_manager pool(options)
79
+
80
+ request_config = RequestConfig.custom
81
+ request_config.set_connection_request_timeout options.fetch(:request_timeout, DEFAULT_REQUEST_TIMEOUT) * 1000
82
+ request_config.set_connect_timeout options.fetch(:connect_timeout, DEFAULT_CONNECT_TIMEOUT) * 1000
83
+ request_config.set_socket_timeout options.fetch(:socket_timeout, DEFAULT_SOCKET_TIMEOUT) * 1000
84
+ request_config.set_max_redirects options.fetch(:max_redirects, DEFAULT_MAX_REDIRECTS)
85
+ request_config.set_expect_continue_enabled options.fetch(:expect_continue, DEFAULT_EXPECT_CONTINUE)
86
+ request_config.set_stale_connection_check_enabled options.fetch(:stale_check, DEFAULT_STALE_CHECK)
87
+ # request_config.set_authentication_enabled options.fetch(:use_auth, false)
88
+ request_config.set_circular_redirects_allowed false
89
+
90
+ yield builder, request_config if block_given?
91
+
92
+ builder.set_default_request_config request_config.build
93
+ @client = builder.build
94
+ @options = options
95
+ @async_requests = []
96
+ end
97
+
98
+ ### Sync methods
99
+
100
+ # Perform a HTTP GET request
101
+ # @param url [String] URL to request
102
+ # @param options [Hash]
103
+ # @option options [Hash] params Hash of options to pass as request parameters
104
+ # @option options [Hash] headers Hash of options to pass as additional request headers
105
+ #
106
+ # @return [Response]
107
+ def get(url, options = {}, &block)
108
+ request HttpGet, url, options, &block
109
+ end
110
+
111
+ # Perform a HTTP PUT request
112
+ # @param url [String] URL to request
113
+ # @param options [Hash]
114
+ # @option options [Hash] params Hash of options to pass as request parameters
115
+ # @option options [Hash] body Hash of options to pass as request body
116
+ # @option options [Hash] headers Hash of options to pass as additional request headers
117
+ #
118
+ # @return [Response]
119
+ def put(url, options = {}, &block)
120
+ request HttpPut, url, options, &block
121
+ end
122
+
123
+ # Perform a HTTP HEAD request
124
+ # @param url [String] URL to request
125
+ # @param options [Hash]
126
+ # @option options [Hash] params Hash of options to pass as request parameters
127
+ # @option options [Hash] headers Hash of options to pass as additional request headers
128
+ #
129
+ # @return [Response]
130
+ def head(url, options = {}, &block)
131
+ request HttpHead, url, options, &block
132
+ end
133
+
134
+ # Perform a HTTP POST request
135
+ # @param url [String] URL to request
136
+ # @param options [Hash]
137
+ # @option options [Hash] params Hash of options to pass as request parameters
138
+ # @option options [Hash] body Hash of options to pass as request body
139
+ # @option options [Hash] headers Hash of options to pass as additional request headers
140
+ #
141
+ # @return [Response]
142
+ def post(url, options = {}, &block)
143
+ request HttpPost, url, options, &block
144
+ end
145
+
146
+ # Perform a HTTP DELETE request
147
+ # @param url [String] URL to request
148
+ # @param options [Hash]
149
+ # @option options [Hash] params Hash of options to pass as request parameters
150
+ # @option options [Hash] headers Hash of options to pass as additional request headers
151
+ #
152
+ # @return [Response]
153
+ def delete(url, options = {}, &block)
154
+ request HttpDelete, url, options, &block
155
+ end
156
+
157
+ # Perform a HTTP OPTIONS request
158
+ # @param url [String] URL to request
159
+ # @param options [Hash]
160
+ # @option options [Hash] params Hash of options to pass as request parameters
161
+ # @option options [Hash] headers Hash of options to pass as additional request headers
162
+ #
163
+ # @return [Response]
164
+ def options(url, options = {}, &block)
165
+ request HttpOptions, url, options, &block
166
+ end
167
+
168
+ # Perform a HTTP PATCH request
169
+ # @param url [String] URL to request
170
+ # @param options [Hash]
171
+ # @option options [Hash] params Hash of options to pass as request parameters
172
+ # @option options [Hash] body Hash of options to pass as request body
173
+ # @option options [Hash] headers Hash of options to pass as additional request headers
174
+ #
175
+ # @return [Response]
176
+ def patch(url, options = {}, &block)
177
+ request HttpPatch, url, options, &block
178
+ end
179
+
180
+ ### Async methods
181
+
182
+ # Queue an asynchronous HTTP GET request
183
+ # @param url [String] URL to request
184
+ # @param options [Hash]
185
+ # @option options [Hash] params Hash of options to pass as request parameters
186
+ # @option options [Hash] headers Hash of options to pass as additional request headers
187
+ #
188
+ # @return [Response]
189
+ def async_get(url, options = {}, &block)
190
+ get url, options.merge(async: true), &block
191
+ end
192
+
193
+ # Queue an asynchronous HTTP HEAD request
194
+ # @param url [String] URL to request
195
+ # @param options [Hash]
196
+ # @option options [Hash] params Hash of options to pass as request parameters
197
+ # @option options [Hash] headers Hash of options to pass as additional request headers
198
+ #
199
+ # @return [Response]
200
+ def async_head(url, options = {}, &block)
201
+ head url, options.merge(async: true), &block
202
+ end
203
+
204
+ # Queue an asynchronous HTTP PUT request
205
+ # @param url [String] URL to request
206
+ # @param options [Hash]
207
+ # @option options [Hash] params Hash of options to pass as request parameters
208
+ # @option options [Hash] body Hash of options to pass as request body
209
+ # @option options [Hash] headers Hash of options to pass as additional request headers
210
+ #
211
+ # @return [Response]
212
+ def async_put(url, options = {}, &block)
213
+ put url, options.merge(async: true), &block
214
+ end
215
+
216
+ # Queue an asynchronous HTTP POST request
217
+ # @param url [String] URL to request
218
+ # @param options [Hash]
219
+ # @option options [Hash] params Hash of options to pass as request parameters
220
+ # @option options [Hash] body Hash of options to pass as request body
221
+ # @option options [Hash] headers Hash of options to pass as additional request headers
222
+ #
223
+ # @return [Response]
224
+ def async_post(url, options = {}, &block)
225
+ post url, options.merge(async: true), &block
226
+ end
227
+
228
+ # Queue an asynchronous HTTP DELETE request
229
+ # @param url [String] URL to request
230
+ # @param options [Hash]
231
+ # @option options [Hash] params Hash of options to pass as request parameters
232
+ # @option options [Hash] headers Hash of options to pass as additional request headers
233
+ #
234
+ # @return [Response]
235
+ def async_delete(url, options = {}, &block)
236
+ delete url, options.merge(async: true), &block
237
+ end
238
+
239
+ # Queue an asynchronous HTTP OPTIONS request
240
+ # @param url [String] URL to request
241
+ # @param options [Hash]
242
+ # @option options [Hash] params Hash of options to pass as request parameters
243
+ # @option options [Hash] headers Hash of options to pass as additional request headers
244
+ #
245
+ # @return [Response]
246
+ def async_options(url, options = {}, &block)
247
+ options url, options.merge(async: true), &block
248
+ end
249
+
250
+ # Queue an asynchronous HTTP PATCH request
251
+ # @param url [String] URL to request
252
+ # @param options [Hash]
253
+ # @option options [Hash] params Hash of options to pass as request parameters
254
+ # @option options [Hash] body Hash of options to pass as request body
255
+ # @option options [Hash] headers Hash of options to pass as additional request headers
256
+ #
257
+ # @return [Response]
258
+ def async_patch(url, options = {}, &block)
259
+ patch url, options.merge(async: true), &block
260
+ end
261
+
262
+ # Remove all pending asynchronous requests.
263
+ #
264
+ # @return nil
265
+ def clear_pending
266
+ @async_requests.clear
267
+ nil
268
+ end
269
+
270
+ # Execute all queued async requests
271
+ #
272
+ # @return [Array] An array of the results of the on_success bodies for the requests executed.
273
+ def execute!
274
+ tasks = @async_requests.map do |request, response|
275
+ task = FutureTask.new(response)
276
+ @executor.submit task
277
+ task
278
+ end
279
+ @async_requests.clear
280
+ tasks.map do |task|
281
+ begin
282
+ response = task.get
283
+ rescue => e
284
+ raise e.getCause
285
+ end
286
+ end
287
+ end
288
+
289
+ protected
290
+
291
+ def client_builder
292
+ HttpClientBuilder.create
293
+ end
294
+
295
+ def pool_builder
296
+ PoolingHttpClientConnectionManager.new
297
+ end
298
+
299
+ def pool(options = {})
300
+ @pool ||= begin
301
+ @max_pool_size = options.fetch(:pool_max, DEFAULT_MAX_POOL_SIZE)
302
+ cm = pool_builder
303
+ cm.set_default_max_per_route options.fetch(:pool_max_per_route, DEFAULT_MAX_PER_ROUTE)
304
+ cm.set_max_total @max_pool_size
305
+ Thread.new {
306
+ loop {
307
+ cm.closeExpiredConnections
308
+ sleep 5000
309
+ }
310
+ }
311
+ cm
312
+ end
313
+ end
314
+
315
+ def create_executor_if_needed
316
+ return @executor if @executor
317
+ @executor = Executors.new_cached_thread_pool
318
+ at_exit { @executor.shutdown }
319
+ end
320
+
321
+ def request(klass, url, options, &block)
322
+ req = request_from_options(klass, url, options)
323
+ if options.delete(:async)
324
+ async_request req, &block
325
+ else
326
+ sync_request req, &block
327
+ end
328
+ end
329
+
330
+ def async_request(request, &block)
331
+ create_executor_if_needed
332
+ response = AsyncResponse.new(@client, request, BasicHttpContext.new, block)
333
+ @async_requests << [request, response]
334
+ response
335
+ end
336
+
337
+ def sync_request(request, &block)
338
+ response = Response.new(request, BasicHttpContext.new, block)
339
+ begin
340
+ @client.execute request, response, response.context
341
+ response
342
+ rescue Java::JavaNet::SocketTimeoutException, Java::OrgApacheHttpConn::ConnectTimeoutException, Java::OrgApacheHttp::NoHttpResponseException => e
343
+ raise Manticore::Timeout.new(e.get_cause)
344
+ rescue Java::OrgApacheHttpClient::ClientProtocolException => e
345
+ raise Manticore::ClientProtocolException.new(e.get_cause)
346
+ end
347
+ end
348
+
349
+ def uri_from_url_and_options(url, options)
350
+ uri = Addressable::URI.parse url
351
+ if options[:query]
352
+ uri.query_values ||= {}
353
+ case options[:query]
354
+ when Hash
355
+ uri.query_values.merge! options[:query]
356
+ when String
357
+ uri.query_values.merge! CGI.parse(options[:query])
358
+ else
359
+ raise "Queries must be hashes or strings"
360
+ end
361
+ end
362
+ uri
363
+ end
364
+
365
+ def request_from_options(klass, url, options)
366
+ req = klass.new uri_from_url_and_options(url, options).to_s
367
+
368
+ if ( options[:params] || options[:body] ) &&
369
+ ( req.instance_of?(HttpPost) || req.instance_of?(HttpPatch) || req.instance_of?(HttpPut) )
370
+ if options[:params]
371
+ req.set_entity hash_to_entity(options[:params])
372
+ elsif options[:body]
373
+ req.set_entity StringEntity.new(options[:body])
374
+ end
375
+ end
376
+
377
+ if options[:headers]
378
+ options[:headers].each {|k, v| req.set_header k, v }
379
+ end
380
+
381
+ req
382
+ end
383
+
384
+ def hash_to_entity(hash)
385
+ pairs = hash.map do |key, val|
386
+ BasicNameValuePair.new(key, val)
387
+ end
388
+ UrlEncodedFormEntity.new(pairs)
389
+ end
390
+ end
391
+ end
@@ -0,0 +1,40 @@
1
+ require 'forwardable'
2
+
3
+ module Manticore
4
+ # Mix-in that can be used to add Manticore functionality to arbitrary classes.
5
+ # Its primary purpose is to extend the Manticore module for one-shot usage.
6
+ #
7
+ # @example Simple mix-in usage
8
+ # class Foo
9
+ # include Manticore::Facade
10
+ # include_http_client pool_size: 5
11
+ #
12
+ # def latest_results
13
+ # Foo.get "http://some.com/url"
14
+ # end
15
+ # end
16
+ module Facade
17
+ # @private
18
+ def self.included(other)
19
+ other.send :extend, ClassMethods
20
+ end
21
+
22
+ module ClassMethods
23
+ # Adds basic synchronous Manticore::Client functionality to the receiver.
24
+ # @param options Hash Options to be passed to the underlying shared client, if it has not been created yet.
25
+ # @return nil
26
+ def include_http_client(options = {}, &block)
27
+ if shared_pool = options.delete(:shared_pool)
28
+ @manticore_facade = Manticore.instance_variable_get("@manticore_facade")
29
+ else
30
+ @manticore_facade = Manticore::Client.new(options, &block)
31
+ end
32
+ class << self
33
+ extend Forwardable
34
+ def_delegators "@manticore_facade", :get, :head, :put, :post, :delete, :options, :patch
35
+ end
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,76 @@
1
+ module Manticore
2
+ # Implementation of {http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/ResponseHandler.html ResponseHandler} which serves
3
+ # as a Ruby proxy for HTTPClient responses.
4
+ #
5
+ # @!attribute [r] headers
6
+ # @return [Hash] Headers from this response
7
+ # @!attribute [r] code
8
+ # @return [Integer] Response code from this response
9
+ # @!attribute [r] context
10
+ # @return [HttpContext] Context associated with this request/response
11
+ class Response
12
+ include_package "org.apache.http.client"
13
+ include_package "org.apache.http.util"
14
+ include_package "org.apache.http.protocol"
15
+ include ResponseHandler
16
+
17
+ attr_reader :headers, :code, :context
18
+
19
+ # Creates a new Response
20
+ #
21
+ # @param request [HttpRequestBase] The underlying request object
22
+ # @param context [HttpContext] The underlying HttpContext
23
+ # @param body_handler_block [Proc] And optional block to by yielded to for handling this response
24
+ def initialize(request, context, body_handler_block)
25
+ @request = request
26
+ @context = context
27
+ @handler_block = body_handler_block
28
+ end
29
+
30
+ # Implementation of {http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/ResponseHandler.html#handleResponse(org.apache.http.HttpResponse) ResponseHandler#handleResponse}
31
+ # @param response [Response] The underlying Java Response object
32
+ def handle_response(response)
33
+ @response = response
34
+ @code = response.get_status_line.get_status_code
35
+ @headers = Hash[* response.get_all_headers.flat_map {|h| [h.get_name.downcase, h.get_value]} ]
36
+ if @handler_block
37
+ @handler_block.call(self)
38
+ else
39
+ read_body
40
+ end
41
+ # ensure
42
+ # @request.release_connection
43
+ end
44
+
45
+ # Fetch the final resolved URL for this response
46
+ #
47
+ # @return [String]
48
+ def final_url
49
+ last_request = context.get_attribute ExecutionContext.HTTP_REQUEST
50
+ last_host = context.get_attribute ExecutionContext.HTTP_TARGET_HOST
51
+ host = last_host.to_uri
52
+ url = last_request.get_uri
53
+ URI.join(host, url.to_s)
54
+ end
55
+
56
+ # Fetch the body content of this response
57
+ #
58
+ # @return [String] Reponse body
59
+ def read_body
60
+ @body ||= begin
61
+ entity = @response.get_entity
62
+ entity && EntityUtils.to_string(entity)
63
+ rescue Java::JavaIo::IOException, Java::JavaNet::SocketException => e
64
+ raise StreamClosedException.new("Could not read from stream: #{e.message} (Did you forget to read #body from your block?)")
65
+ end
66
+ end
67
+ alias_method :body, :read_body
68
+
69
+ # Returns the length of the response body. Returns -1 if content-length is not present in the response.
70
+ #
71
+ # @return [Integer]
72
+ def length
73
+ (@headers["content-length"] || -1).to_i
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Manticore
2
+ VERSION = "0.1.0"
3
+ end
data/lib/manticore.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'java'
2
+ require_relative "./jar/httpcore-4.3.1"
3
+ require_relative "./jar/httpclient-4.3.2"
4
+ require_relative "./jar/commons-logging-1.1.3"
5
+ require_relative "./manticore/version"
6
+ require "addressable/uri"
7
+
8
+ # HTTP client with the body of a lion and the head of a man. Or more simply, the power of Java
9
+ # with the beauty of Ruby.
10
+ module Manticore
11
+ # General base class for all Manticore exceptions
12
+ class ManticoreException < StandardError; end
13
+
14
+ # Exception thrown if you attempt to read from a closed Response stream
15
+ class StreamClosedException < ManticoreException; end
16
+
17
+ # Friendly wrapper for various Java ClientProtocolExceptions
18
+ class ClientProtocolException < ManticoreException; end
19
+
20
+ require_relative './manticore/client'
21
+ require_relative './manticore/response'
22
+ require_relative './manticore/async_response'
23
+ require_relative './manticore/facade'
24
+
25
+ include Facade
26
+ include_http_client
27
+ end
data/manticore.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'manticore/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "manticore"
8
+ spec.version = Manticore::VERSION
9
+ spec.authors = ["Chris Heald"]
10
+ spec.email = ["cheald@mashable.com"]
11
+ spec.description = %q{Manticore is an HTTP client built on the Apache HttpCore components}
12
+ spec.summary = %q{Manticore is an HTTP client built on the Apache HttpCore components}
13
+ spec.homepage = "https://github.com/cheald/manticore"
14
+ spec.license = "MIT"
15
+ spec.platform = 'java'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "addressable", "~> 2.3"
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end