mingle_events 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -4
- data/lib/mingle_events.rb +19 -3
- data/lib/mingle_events/entry_cache.rb +129 -0
- data/lib/mingle_events/feed/author.rb +12 -23
- data/lib/mingle_events/feed/category.rb +2 -0
- data/lib/mingle_events/feed/changes.rb +19 -50
- data/lib/mingle_events/feed/entry.rb +16 -21
- data/lib/mingle_events/feed/links.rb +3 -3
- data/lib/mingle_events/feed/page.rb +5 -5
- data/lib/mingle_events/http.rb +50 -0
- data/lib/mingle_events/mingle_basic_auth_access.rb +17 -50
- data/lib/mingle_events/mingle_hmac_auth_access.rb +22 -0
- data/lib/mingle_events/mingle_oauth_access.rb +10 -30
- data/lib/mingle_events/poller.rb +4 -3
- data/lib/mingle_events/processors/author_filter.rb +7 -7
- data/lib/mingle_events/processors/card_data.rb +15 -15
- data/lib/mingle_events/project_custom_properties.rb +15 -15
- data/lib/mingle_events/project_event_fetcher.rb +21 -110
- data/lib/mingle_events/xml.rb +101 -0
- data/lib/mingle_events/zip_directory.rb +98 -0
- data/test/mingle_events/entry_cache_test.rb +75 -0
- data/test/mingle_events/feed/author_test.rb +2 -2
- data/test/mingle_events/feed/changes_test.rb +22 -22
- data/test/mingle_events/feed/entry_test.rb +12 -12
- data/test/mingle_events/feed/links_test.rb +5 -5
- data/test/mingle_events/mingle_basic_auth_access_test.rb +15 -0
- data/test/mingle_events/mingle_hmac_auth_access_test.rb +15 -0
- data/test/mingle_events/mingle_oauth_access_test.rb +15 -0
- data/test/mingle_events/poller_test.rb +1 -1
- data/test/mingle_events/processors/author_filter_test.rb +5 -5
- data/test/mingle_events/processors/card_data_test.rb +12 -17
- data/test/mingle_events/project_custom_properties_test.rb +2 -3
- data/test/mingle_events/project_event_fetcher_test.rb +64 -8
- data/test/mingle_events/xml_test.rb +80 -0
- data/test/mingle_events/zip_directory_test.rb +44 -0
- data/test/test_helper.rb +36 -27
- metadata +121 -62
@@ -14,27 +14,27 @@ module MingleEvents
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def entries
|
17
|
-
@entries ||= page_as_document.
|
17
|
+
@entries ||= page_as_document.select_all('./atom:feed/atom:entry').map do |entry_element|
|
18
18
|
Entry.new(entry_element)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
def next
|
23
|
-
next_url_element = page_as_document.
|
23
|
+
next_url_element = page_as_document.select("./atom:feed/atom:link[@rel='next']")
|
24
24
|
if next_url_element.nil?
|
25
25
|
nil
|
26
26
|
else
|
27
|
-
Page.new(next_url_element
|
27
|
+
Page.new(next_url_element.attr("href"), @mingle_access)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
33
|
def page_as_document
|
34
|
-
@page_as_document ||=
|
34
|
+
@page_as_document ||= Xml.parse(@mingle_access.fetch_page(@url), ATOM_AND_MINGLE_NS)
|
35
35
|
end
|
36
36
|
|
37
37
|
end
|
38
38
|
|
39
39
|
end
|
40
|
-
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Http
|
3
|
+
extend self
|
4
|
+
|
5
|
+
MAX_RETRY_TIMES = 5
|
6
|
+
|
7
|
+
# get response body for a url, a block can be passed in for request pre-processing
|
8
|
+
def get(url, retry_count=0, &block)
|
9
|
+
rsp = fetch_page_response(url, &block)
|
10
|
+
case rsp
|
11
|
+
when Net::HTTPSuccess
|
12
|
+
rsp.body
|
13
|
+
when Net::HTTPUnauthorized
|
14
|
+
raise HttpError.new(rsp, url, %{
|
15
|
+
If you think you are passing correct credentials, please check
|
16
|
+
that you have enabled Mingle for basic authentication.
|
17
|
+
See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_authentication.html>.})
|
18
|
+
when Net::HTTPBadGateway, Net::HTTPServiceUnavailable, Net::HTTPGatewayTimeOut
|
19
|
+
raise HttpError.new(rsp, url) if retry_count >= MAX_RETRY_TIMES
|
20
|
+
cooldown = retry_count * 2
|
21
|
+
MingleEvents.log.info "Getting service error when get page at #{url}, retry after #{cooldown}s..."
|
22
|
+
sleep cooldown
|
23
|
+
get(url, retry_count + 1, &block)
|
24
|
+
else
|
25
|
+
raise HttpError.new(rsp, url)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def fetch_page_response(url, &block)
|
31
|
+
uri = URI.parse(url)
|
32
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
33
|
+
path = uri.request_uri
|
34
|
+
|
35
|
+
if uri.scheme == 'https'
|
36
|
+
http.use_ssl = true
|
37
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
38
|
+
end
|
39
|
+
|
40
|
+
MingleEvents.log.info "Fetching page at #{path}..."
|
41
|
+
|
42
|
+
start = Time.now
|
43
|
+
req = Net::HTTP::Get.new(path)
|
44
|
+
yield(req) if block_given?
|
45
|
+
rsp = http.request(req)
|
46
|
+
MingleEvents.log.info "...#{path} fetched in #{Time.now - start} seconds."
|
47
|
+
rsp
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,70 +1,37 @@
|
|
1
|
-
module MingleEvents
|
2
|
-
|
1
|
+
module MingleEvents
|
2
|
+
|
3
3
|
# Supports fetching of Mingle resources using HTTP basic auth.
|
4
4
|
# Please only use this class to access resources over HTTPS so
|
5
5
|
# as not to send credentials over plain-text connections.
|
6
6
|
class MingleBasicAuthAccess
|
7
|
-
|
8
|
-
attr_reader :base_url
|
9
|
-
|
10
|
-
BASIC_AUTH_HTTP_WARNING = %{
|
7
|
+
BASIC_AUTH_HTTP_WARNING = %{
|
11
8
|
WARNING!!!
|
12
|
-
It looks like you are using basic authentication over a plain-text HTTP connection.
|
9
|
+
It looks like you are using basic authentication over a plain-text HTTP connection.
|
13
10
|
We HIGHLY recommend AGAINST this practice. You should only use basic authentication over
|
14
11
|
a secure HTTPS connection. Instructions for enabling HTTPS/SSL in Mingle can be found at
|
15
12
|
<http://www.thoughtworks-studios.com/mingle/3.3/help/advanced_mingle_configuration.html>
|
16
13
|
WARNING!!
|
17
14
|
}
|
15
|
+
attr_reader :base_url
|
18
16
|
|
19
|
-
def initialize(base_url, username, password)
|
17
|
+
def initialize(base_url, username, password, http=Http)
|
20
18
|
@base_url = base_url
|
21
19
|
@username = username
|
22
20
|
@password = password
|
21
|
+
@http = http
|
23
22
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
rsp.body
|
31
|
-
when Net::HTTPUnauthorized
|
32
|
-
raise HttpError.new(rsp, location, %{
|
33
|
-
If you think you are passing correct credentials, please check
|
34
|
-
that you have enabled Mingle for basic authentication.
|
35
|
-
See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_authentication.html>.})
|
36
|
-
else
|
37
|
-
raise HttpError.new(rsp, location)
|
23
|
+
|
24
|
+
def fetch_page(location)
|
25
|
+
location = @base_url + location if location[0..0] == '/'
|
26
|
+
@http.get(location) do |req|
|
27
|
+
MingleEvents.log.warn(BASIC_AUTH_HTTP_WARNING) if URI.parse(location).scheme == 'http'
|
28
|
+
req['authorization'] = basic_encode(@username, @password)
|
38
29
|
end
|
39
30
|
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def fetch_page_response(location)
|
44
|
-
location = @base_url + location if location[0..0] == '/'
|
45
|
-
|
46
|
-
uri = URI.parse(location)
|
47
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
48
|
-
|
49
|
-
if uri.scheme == 'https'
|
50
|
-
http.use_ssl = true
|
51
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
52
|
-
else
|
53
|
-
MingleEvents.log.warn BASIC_AUTH_HTTP_WARNING
|
54
|
-
end
|
55
|
-
|
56
|
-
path = uri.path
|
57
|
-
path += "?#{uri.query}" if uri.query
|
58
|
-
MingleEvents.log.info "Fetching page at #{path}..."
|
59
|
-
|
60
|
-
start = Time.now
|
61
|
-
req = Net::HTTP::Get.new(path)
|
62
|
-
req.basic_auth(@username, @password)
|
63
|
-
rsp = http.request(req)
|
64
|
-
MingleEvents.log.info "...#{path} fetched in #{Time.now - start} seconds."
|
65
31
|
|
66
|
-
|
32
|
+
private
|
33
|
+
def basic_encode(account, password)
|
34
|
+
'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
|
67
35
|
end
|
68
|
-
|
69
36
|
end
|
70
|
-
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
# Client for Mingle's experimental HMAC api auth support
|
3
|
+
class MingleHmacAuthAccess
|
4
|
+
attr_reader :base_url
|
5
|
+
|
6
|
+
def initialize(base_url, login, api_key, http=Http)
|
7
|
+
@base_url = base_url
|
8
|
+
@login = login
|
9
|
+
@api_key = api_key
|
10
|
+
@http = http
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch_page(location)
|
14
|
+
location = @base_url + location if location[0..0] == '/'
|
15
|
+
@http.get(location) do |req|
|
16
|
+
ApiAuth.sign!(req, @login, @api_key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
end
|
@@ -1,40 +1,20 @@
|
|
1
|
-
module MingleEvents
|
2
|
-
|
1
|
+
module MingleEvents
|
2
|
+
|
3
3
|
# Client for Mingle's experimental OAuth 2.0 support in 3.0
|
4
|
-
#--
|
5
|
-
# TODO: Update error handling and support of fetching response
|
6
|
-
# objects to that of MingleBasicAuthAccess
|
7
4
|
class MingleOauthAccess
|
5
|
+
attr_reader :base_url
|
8
6
|
|
9
|
-
def initialize(base_url, token)
|
7
|
+
def initialize(base_url, token, http=Http)
|
10
8
|
@base_url = base_url
|
11
9
|
@token = token
|
10
|
+
@http = http
|
12
11
|
end
|
13
12
|
|
14
13
|
def fetch_page(location)
|
15
|
-
location
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
http.use_ssl = true
|
20
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
21
|
-
headers = {
|
22
|
-
'Authorization' => %{Token token="#{@token}"}
|
23
|
-
}
|
24
|
-
|
25
|
-
path = uri.path
|
26
|
-
path += "?#{uri.query}" if uri.query
|
27
|
-
MingleEvents.log.info "Fetching page at #{path}..."
|
28
|
-
|
29
|
-
start = Time.now
|
30
|
-
response = http.get(path, headers)
|
31
|
-
MingleEvents.log.info "... #{path} fetched in #{Time.now - start} seconds."
|
32
|
-
|
33
|
-
# todo: what's the right way to raise on non 200 ?
|
34
|
-
# raise StandardError.new(response.body) unless response == Net::HTTPSuccess
|
35
|
-
|
36
|
-
response.body
|
14
|
+
location = @base_url + location if location[0..0] == '/'
|
15
|
+
@http.get(location) do |req|
|
16
|
+
req['Authorization'] = %{Token token="#{@token}"}
|
17
|
+
end
|
37
18
|
end
|
38
|
-
|
39
19
|
end
|
40
|
-
end
|
20
|
+
end
|
data/lib/mingle_events/poller.rb
CHANGED
@@ -5,17 +5,18 @@ module MingleEvents
|
|
5
5
|
# configured for specified mingle projects. processors_by_project_identifier should
|
6
6
|
# be a hash where the keys are mingle project identifiers and the values are
|
7
7
|
# lists of event processors.
|
8
|
-
def initialize(mingle_access, processors_by_project_identifier)
|
8
|
+
def initialize(mingle_access, processors_by_project_identifier, state_dir=nil)
|
9
9
|
@mingle_access = mingle_access
|
10
10
|
@processors_by_project_identifier = processors_by_project_identifier
|
11
|
+
@state_dir = state_dir
|
11
12
|
end
|
12
13
|
|
13
14
|
# Run a single poll for each project configured with processor(s) and
|
14
15
|
# broadcast each event to each processor.
|
15
|
-
def run_once
|
16
|
+
def run_once
|
16
17
|
MingleEvents.log.info("MingleEvents::Poller about to poll once...")
|
17
18
|
@processors_by_project_identifier.each do |project_identifier, processors|
|
18
|
-
fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access)
|
19
|
+
fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access, @state_dir)
|
19
20
|
fetcher.set_current_state_to_now_if_no_current_state
|
20
21
|
latest_events = fetcher.fetch_latest.to_a
|
21
22
|
processors.each{|p| p.process_events(latest_events)}
|
@@ -36,14 +36,14 @@ module MingleEvents
|
|
36
36
|
|
37
37
|
def lookup_author_uri
|
38
38
|
team_resource = "/api/v2/projects/#{@project_identifier}/team.xml"
|
39
|
-
@raw_xml ||= @mingle_access.fetch_page(
|
40
|
-
@doc ||=
|
39
|
+
@raw_xml ||= @mingle_access.fetch_page(URIParser.escape(team_resource))
|
40
|
+
@doc ||= Xml.parse(@raw_xml)
|
41
41
|
|
42
|
-
users = @doc.
|
42
|
+
users = @doc.select_all('/projects_members/projects_member/user').map do |user|
|
43
43
|
{
|
44
|
-
:url => user.
|
45
|
-
:login => user.
|
46
|
-
:email => user.
|
44
|
+
:url => user.attr('url'),
|
45
|
+
:login => user.inner_text('login'),
|
46
|
+
:email => user.inner_text('email')
|
47
47
|
}
|
48
48
|
end
|
49
49
|
|
@@ -59,4 +59,4 @@ module MingleEvents
|
|
59
59
|
|
60
60
|
end
|
61
61
|
end
|
62
|
-
end
|
62
|
+
end
|
@@ -52,7 +52,7 @@ module MingleEvents
|
|
52
52
|
# if there's a real problem. In most polling scenarios, this is a highly unlikely
|
53
53
|
# problem as there will usually be 1 or a few events.
|
54
54
|
begin
|
55
|
-
raw_xml = @mingle_access.fetch_page(
|
55
|
+
raw_xml = @mingle_access.fetch_page(URIParser.escape(path))
|
56
56
|
rescue
|
57
57
|
msg = %{
|
58
58
|
|
@@ -71,21 +71,21 @@ Stack Trace:
|
|
71
71
|
MingleEvents.log.info(msg)
|
72
72
|
return
|
73
73
|
end
|
74
|
-
doc =
|
74
|
+
doc = Xml.parse(raw_xml)
|
75
75
|
|
76
|
-
doc.
|
77
|
-
card_number = card_result.
|
78
|
-
card_version = card_result.
|
76
|
+
doc.select_all('/results/result').map do |card_result|
|
77
|
+
card_number = card_result.inner_text('number').to_i
|
78
|
+
card_version = card_result.inner_text('version').to_i
|
79
79
|
custom_properties = {}
|
80
80
|
@card_data_by_number_and_version[data_key(card_number, card_version)] = {
|
81
81
|
:number => card_number,
|
82
82
|
:version => card_version,
|
83
|
-
:card_type_name => card_result.
|
83
|
+
:card_type_name => card_result.inner_text('card_type_name'),
|
84
84
|
:custom_properties => custom_properties
|
85
85
|
}
|
86
86
|
card_result.children.each do |child|
|
87
|
-
if child.
|
88
|
-
custom_properties[@custom_properties.property_name_for_column(child.
|
87
|
+
if child.tag_name.index("cp_") == 0
|
88
|
+
custom_properties[@custom_properties.property_name_for_column(child.tag_name)] =
|
89
89
|
nullable_value_from_element(child)
|
90
90
|
end
|
91
91
|
end
|
@@ -95,17 +95,17 @@ Stack Trace:
|
|
95
95
|
def load_card_data_for_event(card_event)
|
96
96
|
begin
|
97
97
|
page_xml = @mingle_access.fetch_page(card_event.card_version_resource_uri)
|
98
|
-
doc =
|
98
|
+
doc = Xml.parse(page_xml)
|
99
99
|
custom_properties = {}
|
100
100
|
result = {
|
101
101
|
:number => card_event.card_number,
|
102
102
|
:version => card_event.version,
|
103
|
-
:card_type_name => doc.
|
103
|
+
:card_type_name => doc.select('/card/card_type/name').inner_text,
|
104
104
|
:custom_properties => custom_properties
|
105
105
|
}
|
106
|
-
doc.
|
107
|
-
custom_properties[property.
|
108
|
-
nullable_value_from_element(property.
|
106
|
+
doc.select_all('/card/properties/property').each do |property|
|
107
|
+
custom_properties[property.inner_text('name')] =
|
108
|
+
nullable_value_from_element(property.select('value'))
|
109
109
|
end
|
110
110
|
|
111
111
|
result
|
@@ -116,9 +116,9 @@ Stack Trace:
|
|
116
116
|
end
|
117
117
|
|
118
118
|
def nullable_value_from_element(element)
|
119
|
-
element
|
119
|
+
element.attr('nil') == 'true' ? nil : element.inner_text
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
123
|
end
|
124
|
-
end
|
124
|
+
end
|
@@ -1,33 +1,33 @@
|
|
1
1
|
module MingleEvents
|
2
|
-
|
2
|
+
|
3
3
|
class ProjectCustomProperties
|
4
|
-
|
4
|
+
|
5
5
|
def initialize(mingle_access, project_identifier)
|
6
6
|
@mingle_access = mingle_access
|
7
7
|
@project_identifier = project_identifier
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def property_name_for_column(column_name)
|
11
11
|
property_names_by_column_name[column_name]
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
private
|
15
|
-
|
15
|
+
|
16
16
|
def property_names_by_column_name
|
17
17
|
@property_names_by_column_name ||= lookup_property_names_by_column_name
|
18
18
|
end
|
19
|
-
|
20
|
-
def lookup_property_names_by_column_name
|
21
|
-
as_document.
|
22
|
-
mapping[element.
|
19
|
+
|
20
|
+
def lookup_property_names_by_column_name
|
21
|
+
as_document.select_all('/property_definitions/property_definition').inject({}) do |mapping, element|
|
22
|
+
mapping[element.inner_text('column_name')] = element.inner_text('name')
|
23
23
|
mapping
|
24
24
|
end
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def as_document
|
28
|
-
@as_document ||=
|
29
|
-
end
|
30
|
-
|
28
|
+
@as_document ||= Xml.parse(@mingle_access.fetch_page("/api/v2/projects/#{@project_identifier}/property_definitions.xml"))
|
29
|
+
end
|
30
|
+
|
31
31
|
end
|
32
|
-
|
33
|
-
end
|
32
|
+
|
33
|
+
end
|
@@ -7,160 +7,71 @@ module MingleEvents
|
|
7
7
|
# one at at time, reading it off disk, to avoid massive memory consumption.
|
8
8
|
class ProjectEventFetcher
|
9
9
|
|
10
|
+
attr_reader :entry_cache
|
11
|
+
|
10
12
|
def initialize(project_identifier, mingle_access, state_dir=nil)
|
11
13
|
@project_identifier = project_identifier
|
12
14
|
@mingle_access = mingle_access
|
13
15
|
base_uri = URI.parse(mingle_access.base_url)
|
14
|
-
@state_dir =
|
16
|
+
@state_dir = state_dir || File.join('~', '.mingle_events', base_uri.host, base_uri.port.to_s)
|
17
|
+
@state_dir = File.expand_path(File.join(@state_dir, project_identifier, 'fetched_events'))
|
18
|
+
@entry_cache = EntryCache.new(@state_dir)
|
15
19
|
end
|
16
20
|
|
17
21
|
# blow away any existing state, when next used to fetch events from mingle
|
18
22
|
# will crawl all the way back to time zero
|
19
23
|
def reset
|
20
|
-
|
24
|
+
@entry_cache.clear
|
21
25
|
end
|
22
26
|
|
23
27
|
def set_current_state_to_now_if_no_current_state
|
24
|
-
return if has_current_state?
|
25
|
-
|
26
|
-
latest_event = page_with_latest_entries.entries.first
|
27
|
-
return if latest_event.nil?
|
28
|
-
write_entry_to_disk(latest_event, nil)
|
29
|
-
update_current_state(latest_event, latest_event)
|
28
|
+
return if @entry_cache.has_current_state?
|
29
|
+
@entry_cache.set_current_state(page_with_latest_entries.entries.first)
|
30
30
|
end
|
31
31
|
|
32
32
|
# fetch the latest events from mingle, i.e., the ones not previously seen
|
33
33
|
def fetch_latest
|
34
34
|
page = page_with_latest_entries
|
35
35
|
most_recent_new_entry = page.entries.first
|
36
|
-
last_fetched_entry =
|
36
|
+
last_fetched_entry = @entry_cache.latest
|
37
37
|
last_fetched_entry_seen = false
|
38
38
|
next_entry = nil
|
39
39
|
while !last_fetched_entry_seen && page
|
40
40
|
page.entries.each do |entry|
|
41
41
|
|
42
|
+
@entry_cache.write(entry, next_entry)
|
42
43
|
if last_fetched_entry && entry.entry_id == last_fetched_entry.entry_id
|
43
44
|
last_fetched_entry_seen = true
|
44
45
|
break
|
45
46
|
end
|
46
|
-
|
47
|
-
write_entry_to_disk(entry, next_entry)
|
47
|
+
|
48
48
|
next_entry = entry
|
49
|
-
|
50
49
|
end
|
51
50
|
page = page.next
|
52
51
|
end
|
53
52
|
|
54
|
-
update_current_state(next_entry, most_recent_new_entry)
|
55
|
-
|
53
|
+
@entry_cache.update_current_state(next_entry, most_recent_new_entry)
|
54
|
+
@entry_cache.entries(next_entry, most_recent_new_entry)
|
56
55
|
end
|
57
56
|
|
58
57
|
# returns all previously fetched entries; can be used to reprocess the events for, say,
|
59
58
|
# various historical analyses
|
60
59
|
def all_fetched_entries
|
61
|
-
|
62
|
-
Entries.new(current_state[:first_fetched_entry_info_file], current_state[:last_fetched_entry_info_file])
|
63
|
-
end
|
64
|
-
|
65
|
-
def first_entry_fetched
|
66
|
-
current_state_entry(:first_fetched_entry_info_file)
|
60
|
+
@entry_cache.all_entries
|
67
61
|
end
|
68
62
|
|
69
|
-
def
|
70
|
-
|
63
|
+
def first_entry_fetched
|
64
|
+
@entry_cache.first
|
71
65
|
end
|
72
66
|
|
73
|
-
|
74
|
-
|
75
|
-
current_state = load_current_state
|
76
|
-
# if most_recent_new_entry
|
77
|
-
current_state.merge!(:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry))
|
78
|
-
if current_state[:first_fetched_entry_info_file].nil?
|
79
|
-
current_state.merge!(:first_fetched_entry_info_file => file_for_entry(oldest_new_entry))
|
80
|
-
end
|
81
|
-
File.open(current_state_file, 'w'){|out| YAML.dump(current_state, out)}
|
82
|
-
# end
|
83
|
-
end
|
84
|
-
|
85
|
-
# only public to facilitate testing
|
86
|
-
def write_entry_to_disk(entry, next_entry)
|
87
|
-
file = file_for_entry(entry)
|
88
|
-
FileUtils.mkdir_p(File.dirname(file))
|
89
|
-
file_content = {:entry_xml => entry.raw_xml, :next_entry_file_path => file_for_entry(next_entry)}
|
90
|
-
File.open(file, 'w'){|out| YAML.dump(file_content, out)}
|
67
|
+
def last_entry_fetched
|
68
|
+
@entry_cache.latest
|
91
69
|
end
|
92
|
-
|
70
|
+
|
93
71
|
private
|
94
72
|
|
95
73
|
def page_with_latest_entries
|
96
74
|
Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
|
97
|
-
end
|
98
|
-
|
99
|
-
def current_state_entry(info_file_key)
|
100
|
-
if info_file = load_current_state[info_file_key]
|
101
|
-
Feed::Entry.from_snippet(YAML.load(File.new(info_file))[:entry_xml])
|
102
|
-
else
|
103
|
-
nil
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def file_for_entry(entry)
|
108
|
-
return nil if entry.nil?
|
109
|
-
|
110
|
-
entry_id_as_uri = URI.parse(entry.entry_id)
|
111
|
-
relative_path_parts = entry_id_as_uri.path.split('/')
|
112
|
-
entry_id_int = relative_path_parts.last
|
113
|
-
insertions = ["#{entry_id_int.to_i/16384}", "#{entry_id_int.to_i%16384}"]
|
114
|
-
relative_path_parts = relative_path_parts[0..-2] + insertions + ["#{entry_id_int}.yml"]
|
115
|
-
File.join(@state_dir, *relative_path_parts)
|
116
|
-
end
|
117
|
-
|
118
|
-
def has_current_state?
|
119
|
-
File.exist?(current_state_file)
|
120
|
-
end
|
121
|
-
|
122
|
-
def current_state_file
|
123
|
-
File.expand_path(File.join(@state_dir, 'current_state.yml'))
|
124
|
-
end
|
125
|
-
|
126
|
-
def load_last_fetched_entry
|
127
|
-
current_state = load_current_state
|
128
|
-
last_fetched_entry = if current_state[:last_fetched_entry_info_file]
|
129
|
-
last_fetched_entry_info = YAML.load(File.new(current_state[:last_fetched_entry_info_file]))
|
130
|
-
Feed::Entry.from_snippet(last_fetched_entry_info[:entry_xml])
|
131
|
-
else
|
132
|
-
nil
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def load_current_state
|
137
|
-
if has_current_state?
|
138
|
-
YAML.load(File.new(current_state_file))
|
139
|
-
else
|
140
|
-
{:last_fetched_entry_info_file => nil, :first_fetched_entry_info_file => nil}
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
class Entries
|
145
|
-
|
146
|
-
include Enumerable
|
147
|
-
|
148
|
-
def initialize(first_info_file, last_info_file)
|
149
|
-
@first_info_file = first_info_file
|
150
|
-
@last_info_file = last_info_file
|
151
|
-
end
|
152
|
-
|
153
|
-
def each(&block)
|
154
|
-
current_file = @first_info_file
|
155
|
-
while current_file
|
156
|
-
current_entry_info = YAML.load(File.new(current_file))
|
157
|
-
yield(Feed::Entry.from_snippet(current_entry_info[:entry_xml]))
|
158
|
-
break if File.expand_path(current_file) == File.expand_path(@last_info_file)
|
159
|
-
current_file = current_entry_info[:next_entry_file_path]
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
end
|
164
|
-
|
75
|
+
end
|
165
76
|
end
|
166
|
-
end
|
77
|
+
end
|