conflict 0.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/LICENSE +174 -0
- data/README +23 -0
- data/Rakefile +63 -0
- data/bin/conflict +13 -0
- data/lib/conflict.rb +27 -0
- data/lib/conflict/core.rb +170 -0
- data/lib/conflict/domain.rb +89 -0
- data/lib/conflict/parsers.rb +130 -0
- data/lib/run_conflict +26 -0
- data/test/common.rb +132 -0
- data/test/data/a13g.diff.txt +379 -0
- data/test/data/apache_myfaces.diff.txt +22 -0
- data/test/data/rails.diff.txt +13 -0
- data/test/data/rails.info.txt +11 -0
- data/test/data/stomp/stomp.1.diff.txt +21 -0
- data/test/data/stomp/stomp.1.info.txt +11 -0
- data/test/data/stomp/stomp.2.diff.txt +21 -0
- data/test/data/stomp/stomp.2.info.txt +11 -0
- data/test/data/three/added.diff.txt +11 -0
- data/test/data/three/changed.diff.txt +12 -0
- data/test/data/three/conflict.info.txt +11 -0
- data/test/data/three/deleted.diff.txt +11 -0
- data/test/database_test.rb +130 -0
- data/test/equality_test.rb +60 -0
- data/test/event_test.rb +40 -0
- data/test/negative_test.rb +107 -0
- data/test/parser_test.rb +98 -0
- data/test/round_trip/expiration_test.rb +56 -0
- data/test/round_trip/happy_path_test.rb +105 -0
- data/test/round_trip/multi_diff_test.rb +67 -0
- data/test/round_trip/multiple_actions_test.rb +82 -0
- data/test/round_trip/negative_test.rb +79 -0
- data/test/round_trip/relative_path_test.rb +63 -0
- metadata +86 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
module Conflict
|
19
|
+
|
20
|
+
class Conflict
|
21
|
+
|
22
|
+
attr_reader :local, :remote
|
23
|
+
|
24
|
+
def initialize local, remote
|
25
|
+
raise ConflictException::new("local event cannot be nil") if nil.eql?(local)
|
26
|
+
raise ConflictException::new("remote event cannot be nil") if nil.eql?(remote)
|
27
|
+
raise ConflictException::new("local resource '" + local.resource + "' does not match remote resource '" + remote.resource + "'") if ! local.resource.eql?(remote.resource)
|
28
|
+
raise ConflictException::new("local and remote cannot have the same identity") if local == remote
|
29
|
+
#what about = names, not = resources?
|
30
|
+
raise ConflictException::new("local and remote cannot be eql") if local.eql?(remote)
|
31
|
+
# using clone because jvYaml does not currently support aliasing
|
32
|
+
@local, @remote = local.clone, remote.clone
|
33
|
+
end
|
34
|
+
|
35
|
+
def eql? other
|
36
|
+
@local.eql?(other.local) and @remote.eql?(other.remote)
|
37
|
+
end
|
38
|
+
|
39
|
+
yaml_as "tag:java.yaml.org,2002:object:com.thoughtworks.conflict.Conflict"
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class Event
|
44
|
+
|
45
|
+
attr_reader :client, :action, :resource
|
46
|
+
|
47
|
+
# should at least make constants for event types ?
|
48
|
+
def initialize client, action, resource, created=Time.now, ttl=TTL
|
49
|
+
|
50
|
+
raise ConflictException::new("invalid action '" + action.to_s + "'") if ! ['added', 'changed', 'deleted'].index(action)
|
51
|
+
raise ConflictException::new("client must not be empty value") if nil.eql?(client) or client.empty?
|
52
|
+
raise ConflictException::new("resource must not be empty value") if nil.eql?(resource) or resource.empty?
|
53
|
+
raise ConflictException::new("ttl cannot be negative") if ttl < 0
|
54
|
+
@client, @action, @resource = client, action, resource
|
55
|
+
# kept around for staleness expiration
|
56
|
+
@created_time = created
|
57
|
+
# rendered in YAML
|
58
|
+
@created = [created.year, created.month, created.day, created.hour, created.min] * "-"
|
59
|
+
@ttl = ttl
|
60
|
+
end
|
61
|
+
|
62
|
+
def eql? other
|
63
|
+
@client.eql?(other.client) and @action.eql?(other.action) and @resource.eql?(other.resource)
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
self.class.to_s + ":" + @client.to_s + " " + @action.to_s + " " + resource.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_yaml_properties
|
71
|
+
fields = self.instance_variables.clone
|
72
|
+
fields.delete "@created_time" # keep data/time out of integration point ( ruby != java != .net )
|
73
|
+
fields.delete "@ttl"
|
74
|
+
fields
|
75
|
+
end
|
76
|
+
|
77
|
+
yaml_as "tag:java.yaml.org,2002:object:com.thoughtworks.conflict.Event"
|
78
|
+
|
79
|
+
def to_xml
|
80
|
+
'<event client="' + self.client + '" action="' + self.action + '" resource="' + self.resource + '" />'
|
81
|
+
end
|
82
|
+
|
83
|
+
def expires
|
84
|
+
@created_time + @ttl
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
module Conflict
|
19
|
+
|
20
|
+
class RequestParser
|
21
|
+
|
22
|
+
def initialize ttl
|
23
|
+
raise ConflictException::new("ttl cannot be nil") if nil.eql?(ttl)
|
24
|
+
raise ConflictException::new("ttl cannot be negative") if ttl < 0
|
25
|
+
@ttl = ttl
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse diff, info, client
|
29
|
+
|
30
|
+
events = []
|
31
|
+
|
32
|
+
for i in 0..diff.list.size - 1
|
33
|
+
|
34
|
+
diff_data = diff.list[i].to_s.strip
|
35
|
+
info_data = info.list[i].to_s.strip
|
36
|
+
|
37
|
+
raise "no value for info @ index " + i.to_s if nil.eql?(info_data) or info_data.empty?
|
38
|
+
|
39
|
+
lambda {
|
40
|
+
infos = InfoParser::new().parse(info_data)
|
41
|
+
events = events | DiffParser::new(infos.merge({:ttl=>@ttl})).parse(diff_data, client)
|
42
|
+
}.call if ! nil.eql?(diff_data) && ! diff_data.empty?
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
events
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class InfoParser
|
53
|
+
|
54
|
+
@@url, @@path = "URL: ", "Path: "
|
55
|
+
|
56
|
+
def parse info
|
57
|
+
|
58
|
+
raise ConflictException::new("info cannot be nil or empty") if nil.eql?(info) || info.empty?
|
59
|
+
|
60
|
+
properties = {}
|
61
|
+
|
62
|
+
info.split(%r{\n}).each do | line |
|
63
|
+
properties[:url] = line.to_s.sub(@@url, "").chomp if line.to_s.index(@@url) == 0
|
64
|
+
properties[:path] = line.to_s.sub(@@path, "").chomp if line.to_s.index(@@path) == 0
|
65
|
+
end
|
66
|
+
|
67
|
+
properties
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
class DiffParser
|
73
|
+
|
74
|
+
@@key = "Index: "
|
75
|
+
|
76
|
+
def initialize cfg
|
77
|
+
raise ConflictException::new("cfg cannot be nil") if nil.eql?(cfg)
|
78
|
+
raise ConflictException::new("url value cannot be nil or empty") if nil.eql?(cfg[:url]) or cfg[:url].empty?
|
79
|
+
raise ConflictException::new("path value cannot be nil or empty") if nil.eql?(cfg[:path]) or cfg[:path].empty?
|
80
|
+
raise ConflictException::new("ttl value cannot be nil") if nil.eql?(cfg[:ttl])
|
81
|
+
raise ConflictException::new("ttl cannot be negative") if cfg[:ttl] < 0
|
82
|
+
@cfg = cfg
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse diff, client # would be nice to just get a 3rd party for this
|
86
|
+
|
87
|
+
raise ConflictException::new("client cannot by empty") if nil.eql?(client) || client.empty?
|
88
|
+
raise ConflictException::new("diff cannot by empty") if nil.eql?(diff) || diff.empty?
|
89
|
+
events = []
|
90
|
+
resource = ""
|
91
|
+
lines = diff.split(%r{\n})
|
92
|
+
#Conflict.logger.info("received diff w/ " + lines.size.to_s + " line(s)")
|
93
|
+
bit = true
|
94
|
+
now = Time.now # use one var, so all events for a request get same creation time
|
95
|
+
lines.each do |line|
|
96
|
+
if bit
|
97
|
+
if line.to_s.index(@@key) == 0
|
98
|
+
resource = line.to_s.sub(@@key, "")
|
99
|
+
bit = ! bit
|
100
|
+
end
|
101
|
+
else
|
102
|
+
if line.to_s.index("@@ ") == 0
|
103
|
+
action = infer_action line
|
104
|
+
resource_full = infer_resource(@cfg[:url].to_s, @cfg[:path], resource)
|
105
|
+
events << Event::new(client, action, resource_full, now, @cfg[:ttl])
|
106
|
+
bit = ! bit
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
events
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
def infer_action numbers # we are assuming a lot about unified diff here
|
116
|
+
tokens = numbers.split(' ')
|
117
|
+
# had to go backwards for the last three in delete, webrick or the clients are
|
118
|
+
# dropping the '+' character
|
119
|
+
tokens[2][-3,tokens[2].length].eql?("0,0") ? "deleted" : tokens[1].eql?("-0,0") ? "added" : "changed"
|
120
|
+
end
|
121
|
+
|
122
|
+
def infer_resource url, path, resource
|
123
|
+
path = path.tr("\\", "/").chomp
|
124
|
+
resource = resource.tr("\\", "/").chomp
|
125
|
+
url.chomp + ( ".".eql?(path) ? "/" + resource : resource.sub(path, ""))
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
data/lib/run_conflict
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
#!/usr/bin/env ruby
|
19
|
+
require 'conflict'
|
20
|
+
|
21
|
+
port = ARGV.index('-port') ? ARGV[ARGV.index('-port') + 1] : nil
|
22
|
+
ttl = ARGV.index('-ttl') ? ARGV[ARGV.index('-ttl') + 1] : nil
|
23
|
+
|
24
|
+
s = Conflict::Server::new({:port=>port, :ttl=>ttl})
|
25
|
+
trap("INT"){ s.stop }
|
26
|
+
s.start
|
data/test/common.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
|
18
|
+
require 'test/unit'
|
19
|
+
require 'net/http'
|
20
|
+
module Conflict
|
21
|
+
|
22
|
+
class DataBase
|
23
|
+
def presence_of item
|
24
|
+
@db.index(item)
|
25
|
+
end
|
26
|
+
def insert item
|
27
|
+
@db << item
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Client
|
32
|
+
|
33
|
+
def initialize cfg={}
|
34
|
+
@cfg = {}
|
35
|
+
@cfg[:command] = cfg[:command] ||= "svn diff"
|
36
|
+
@cfg[:id] = cfg[:id] ||= "Conflict Ruby client"
|
37
|
+
@cfg[:ip] = cfg[:ip] ||= "127.0.0.1"
|
38
|
+
@cfg[:port] = cfg[:port] ||= PORT
|
39
|
+
@cfg[:poll] = cfg[:poll] ||= 3
|
40
|
+
end
|
41
|
+
|
42
|
+
def resend
|
43
|
+
raise ConflictException::new('do not call resend before calling send') if ! defined?(@last_diff) or ! defined?(@last_info)
|
44
|
+
diff @last_diff, @last_info
|
45
|
+
end
|
46
|
+
|
47
|
+
def diff diff, info
|
48
|
+
|
49
|
+
raise ConflictException::new('info cannot be nil or empty') if nil.eql?(info) || info.empty?
|
50
|
+
|
51
|
+
response = multi_diff [diff], [info] # a multi diff of one
|
52
|
+
@last_diff, @last_info = diff, info
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
def multi_diff diff, info
|
57
|
+
body = {"client" => @cfg[:id]}
|
58
|
+
diff.each do | d | body = body.merge("diff"=>d, "path"=>".") end
|
59
|
+
info.each do | i | body = body.merge("info"=>i) end
|
60
|
+
YAML.load send(DIFF_URL, body)
|
61
|
+
end
|
62
|
+
|
63
|
+
def revert info
|
64
|
+
diff("", info)
|
65
|
+
end
|
66
|
+
|
67
|
+
#make this static?
|
68
|
+
def count
|
69
|
+
body = send(STATUS_URL, {})
|
70
|
+
document = REXML::Document.new(body)
|
71
|
+
document.root.elements['events'].size
|
72
|
+
end
|
73
|
+
|
74
|
+
def poll
|
75
|
+
|
76
|
+
while true
|
77
|
+
diff = `#{@cfg[:command]}`
|
78
|
+
info = `svn info`
|
79
|
+
puts multi_diff([diff], [info]).to_yaml
|
80
|
+
sleep @cfg[:poll]
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
self.class.to_s + " " + @cfg.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def send url, body
|
91
|
+
req = Net::HTTP::Post.new(url)
|
92
|
+
req.set_form_data(body)
|
93
|
+
http = Net::HTTP.new(@cfg[:ip],@cfg[:port])
|
94
|
+
res = http.request(req)
|
95
|
+
res.body
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
module ConflictConstants
|
103
|
+
|
104
|
+
CLIENT_ONE = "unit test"
|
105
|
+
CLIENT_TWO = "unit test 2"
|
106
|
+
CLIENT_THREE = "unit test 3"
|
107
|
+
NO_CONFLICT = [].to_yaml
|
108
|
+
CONTROLLER = "com.thoughtworks.Controller.java"
|
109
|
+
VIEW = "com.thoughtworks.View.java"
|
110
|
+
MODEL = "com.thoughtworks.Model.java"
|
111
|
+
DATA_DIR = './test/data/'
|
112
|
+
STOMP_DIR = DATA_DIR + 'stomp/'
|
113
|
+
STOMP_1_INFO_PATH = STOMP_DIR + 'stomp.1.info.txt'
|
114
|
+
STOMP_2_INFO_PATH = STOMP_DIR + 'stomp.2.info.txt'
|
115
|
+
STOMP_1_DIFF_PATH = STOMP_DIR + 'stomp.1.diff.txt'
|
116
|
+
STOMP_2_DIFF_PATH = STOMP_DIR + 'stomp.2.diff.txt'
|
117
|
+
STOMP_1_URL = 'http://svn.codehaus.org/stomp/ruby/trunk'
|
118
|
+
STOMP_2_URL = 'http://svn.codehaus.org/stomp/ruby/trunk/lib'
|
119
|
+
STOMP_RESOURCE = STOMP_2_URL + "/stomp.rb"
|
120
|
+
CONFLICT_URL = 'https://conflict.svn.sourceforge.net/svnroot/conflict/src/ruby'
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
class File
|
125
|
+
def to_s
|
126
|
+
string = ""
|
127
|
+
self.each do | line | string += line.to_s end
|
128
|
+
self.close
|
129
|
+
string
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
@@ -0,0 +1,379 @@
|
|
1
|
+
Index: asqs.rb
|
2
|
+
===================================================================
|
3
|
+
--- asqs.rb (revision 107)
|
4
|
+
+++ asqs.rb (working copy)
|
5
|
+
@@ -1,354 +0,0 @@
|
6
|
+
-module ActiveMessaging
|
7
|
+
- module Adapters
|
8
|
+
- module AmazonSQS
|
9
|
+
-
|
10
|
+
- class Connection
|
11
|
+
- include ActiveMessaging::Adapter
|
12
|
+
-
|
13
|
+
- register :asqs
|
14
|
+
-
|
15
|
+
- QUEUE_NAME = 1..80
|
16
|
+
- MESSAGE_SIZE = 1..(256 * 1024)
|
17
|
+
- VISIBILITY_TIMEOUT = 0..(24 * 60 * 60)
|
18
|
+
- NUMBER_OF_MESSAGES = 1..255
|
19
|
+
-
|
20
|
+
- #configurable params
|
21
|
+
- attr_accessor :reliable, :reconnectDelay, :access_key_id, :secret_access_key, :aws_version, :content_type, :host, :port, :poll_interval, :cache_queue_list
|
22
|
+
-
|
23
|
+
- #generic init method needed by a13g
|
24
|
+
- def initialize cfg
|
25
|
+
- raise "Must specify a access_key_id" if (cfg[:access_key_id].nil? || cfg[:access_key_id].empty?)
|
26
|
+
- raise "Must specify a secret_access_key" if (cfg[:secret_access_key].nil? || cfg[:secret_access_key].empty?)
|
27
|
+
-
|
28
|
+
- @access_key_id=cfg[:access_key_id]
|
29
|
+
- @secret_access_key=cfg[:secret_access_key]
|
30
|
+
- @aws_version = cfg[:aws_version] || '2006-04-01'
|
31
|
+
- @content_type = cfg[:content_type] || 'text/plain'
|
32
|
+
- @host = cfg[:host] || 'queue.amazonaws.com'
|
33
|
+
- @port = cfg[:port] || 80
|
34
|
+
- @poll_interval = cfg[:poll_interval] || 1
|
35
|
+
- @cache_queue_list = cfg[:cache_queue_list] || true
|
36
|
+
- @reliable = cfg[:reliable] ||= true
|
37
|
+
- @reconnectDelay = cfg[:reconnectDelay] ||= 5
|
38
|
+
-
|
39
|
+
- #initialize the subscriptions and queues
|
40
|
+
- @subscriptions = {}
|
41
|
+
- @current_subscription = 0
|
42
|
+
- queues
|
43
|
+
- end
|
44
|
+
-
|
45
|
+
- def disconnect
|
46
|
+
- #it's an http request - there is no disconnect - ha!
|
47
|
+
- end
|
48
|
+
-
|
49
|
+
- # queue_name string, headers hash
|
50
|
+
- # for sqs, make sure queue exists, if not create, then add to list of polled queues
|
51
|
+
- def subscribe queue_name, message_headers={}
|
52
|
+
- # look at the existing queues, create any that are missing
|
53
|
+
- queue = get_or_create_queue queue_name
|
54
|
+
- if @subscriptions.has_key? queue.name
|
55
|
+
- @subscriptions[queue.name] += 1
|
56
|
+
- else
|
57
|
+
- @subscriptions[queue.name] = 1
|
58
|
+
- end
|
59
|
+
- end
|
60
|
+
-
|
61
|
+
- # queue_name string, headers hash
|
62
|
+
- # for sqs, attempt delete the queues, won't work if not empty, that's ok
|
63
|
+
- def unsubscribe queue_name, message_headers={}
|
64
|
+
- @subscriptions[queue_name] -= 1
|
65
|
+
- @subscriptions.delete(queue_name) if @subscriptions[queue_name] <= 0
|
66
|
+
- end
|
67
|
+
-
|
68
|
+
- # queue_name string, body string, headers hash
|
69
|
+
- # send a single message to a queue
|
70
|
+
- def send queue_name, message_body, message_headers
|
71
|
+
- queue = get_or_create_queue queue_name
|
72
|
+
- send_messsage queue, message_body
|
73
|
+
- end
|
74
|
+
-
|
75
|
+
- # receive a single message from any of the subscribed queues
|
76
|
+
- # check each queue once, then sleep for poll_interval
|
77
|
+
- def receive
|
78
|
+
- raise "No subscriptions to receive messages from." if (@subscriptions.nil? || @subscriptions.empty?)
|
79
|
+
- start = @current_subscription
|
80
|
+
- while true
|
81
|
+
- @current_subscription = ((@current_subscription < @subscriptions.length-1) ? @current_subscription + 1 : 0)
|
82
|
+
- sleep poll_interval if (@current_subscription == start)
|
83
|
+
- queue_name = @subscriptions.keys.sort[@current_subscription]
|
84
|
+
- queue = queues[queue_name]
|
85
|
+
- unless queue.nil?
|
86
|
+
- messages = retrieve_messsages queue, 1
|
87
|
+
- return messages[0] unless (messages.nil? or messages.empty?)
|
88
|
+
- end
|
89
|
+
- end
|
90
|
+
- end
|
91
|
+
-
|
92
|
+
- def received message
|
93
|
+
- delete_message message
|
94
|
+
- end
|
95
|
+
-
|
96
|
+
- protected
|
97
|
+
-
|
98
|
+
- #belows are the methods from the REST API
|
99
|
+
- def create_queue queue_name
|
100
|
+
- validate_queue_name queue_name
|
101
|
+
- validate_queue queue_name, false
|
102
|
+
- response = transmit 'POST', "/?QueueName=#{queue_name}"
|
103
|
+
- add_queue response.get_text("//QueueUrl")
|
104
|
+
- end
|
105
|
+
-
|
106
|
+
- def list_queues queue_name_prefix=nil
|
107
|
+
- validate_queue_name queue_name_prefix unless queue_name_prefix.nil?
|
108
|
+
- response = transmit 'GET', queue_name_prefix.nil? ? '/' : "/?QueueNamePrefix=#{queue_name_prefix}"
|
109
|
+
- response.nodes("//QueueUrl").collect{ |n| add_queue(n.text) }
|
110
|
+
- end
|
111
|
+
-
|
112
|
+
- def delete_queue queue
|
113
|
+
- validate_queue queue
|
114
|
+
- response = transmit 'DELETE', "#{queue.queue_url}", queue.domain
|
115
|
+
- end
|
116
|
+
-
|
117
|
+
- def send_messsage queue, message
|
118
|
+
- validate_queue queue
|
119
|
+
- validate_message message
|
120
|
+
- response = transmit 'PUT', "#{queue.queue_url}/back", queue.domain, message
|
121
|
+
- end
|
122
|
+
-
|
123
|
+
- def set_visibility_timeout queue, timeout
|
124
|
+
- validate_queue queue
|
125
|
+
- validate_timeout timeout
|
126
|
+
- response = transmit 'PUT', "#{queue.queue_url}?VisibilityTimeout=#{timeout}", queue.domain
|
127
|
+
- end
|
128
|
+
-
|
129
|
+
- def retrieve_messsages queue, num_messages=1, timeout=nil
|
130
|
+
- validate_queue queue
|
131
|
+
- validate_number_of_messages num_messages
|
132
|
+
- validate_timeout timeout if timeout
|
133
|
+
- timeout_path = timeout ? "VisibilityTimeout=#{timeout}&" : ''
|
134
|
+
- response = transmit 'GET', "#{queue.queue_url}/front?#{timeout_path}NumberOfMessages=#{num_messages}", queue.domain
|
135
|
+
- response.nodes("//Message").collect{ |n| Message.from_element n, response, queue }
|
136
|
+
- end
|
137
|
+
-
|
138
|
+
- def get_visibility_timeout queue
|
139
|
+
- validate_queue queue
|
140
|
+
- response = transmit 'GET', "#{queue.queue_url}/", queue.domain
|
141
|
+
- response.get_text('//VisibilityTimeout').to_i
|
142
|
+
- end
|
143
|
+
-
|
144
|
+
- def delete_message message
|
145
|
+
- delete_message_by_id message.queue, message.id
|
146
|
+
- end
|
147
|
+
-
|
148
|
+
- def delete_message_by_id queue, message_id
|
149
|
+
- response = transmit 'DELETE', "#{queue.queue_url}/#{message_id}", queue.domain
|
150
|
+
- end
|
151
|
+
-
|
152
|
+
- def peek_message queue, message_id
|
153
|
+
- response = transmit 'GET', "#{queue.queue_url}/#{message_id}", queue.domain
|
154
|
+
- Message.from_element( response.node('//Message'), response, queue)
|
155
|
+
- end
|
156
|
+
-
|
157
|
+
- private
|
158
|
+
-
|
159
|
+
- def queues
|
160
|
+
- return @queues if (@queues && cache_queue_list)
|
161
|
+
- @queues = {}
|
162
|
+
- list_queues.each{|q| @queues[q.name]=q }
|
163
|
+
- return @queues
|
164
|
+
- end
|
165
|
+
-
|
166
|
+
- def get_or_create_queue queue_name
|
167
|
+
- qs = queues
|
168
|
+
- qs.has_key?(queue_name) ? qs[queue_name] : create_queue(queue_name)
|
169
|
+
- end
|
170
|
+
-
|
171
|
+
- def add_queue(url)
|
172
|
+
- q = Queue.from_url url
|
173
|
+
- queues[q.name] = q if cache_queue_list
|
174
|
+
- return q
|
175
|
+
- end
|
176
|
+
-
|
177
|
+
- # method to do the actual send, generic to get, post, delete, etc.
|
178
|
+
- # action - possible values: get, post, delete
|
179
|
+
- def transmit(command, url, h=host, body=nil, headers={}, p=port)
|
180
|
+
- request_headers = create_headers(command, url, headers, body)
|
181
|
+
- request = http_request_factory(command, url, request_headers, body)
|
182
|
+
- tryit = true
|
183
|
+
- begin
|
184
|
+
- while tryit
|
185
|
+
- response = SQSResponse.new(Net::HTTP.start(h, p){ |http| http.request(request) })
|
186
|
+
- tryit = false
|
187
|
+
- end
|
188
|
+
- rescue
|
189
|
+
- raise $! unless reliable
|
190
|
+
- puts "transmit failed, will retry in #{@reconnectDelay} seconds"
|
191
|
+
- sleep @reconnectDelay
|
192
|
+
- end
|
193
|
+
- # p response
|
194
|
+
- # puts "body: #{response.http_response.body}"
|
195
|
+
- check_errors(response)
|
196
|
+
- end
|
197
|
+
-
|
198
|
+
- def create_headers(cmd, url, headers, body)
|
199
|
+
- # set then merge the headers
|
200
|
+
- hdrs = { 'AWS-Version'=>@aws_version,
|
201
|
+
- 'Date'=>Time.now.httpdate,
|
202
|
+
- 'Content-type'=>@content_type }
|
203
|
+
- hdrs['Content-Length'] = body.length.to_s if (body && (cmd=='POST' or cmd=='PUT'))
|
204
|
+
-
|
205
|
+
-
|
206
|
+
- #merge with the passed in headers to allow overrides
|
207
|
+
- hdrs.merge! headers
|
208
|
+
-
|
209
|
+
- # calculate authorization based on set headers
|
210
|
+
- hdrs['Authorization'] = create_authorization_signature(cmd, url, hdrs)
|
211
|
+
- return hdrs
|
212
|
+
- end
|
213
|
+
-
|
214
|
+
- def create_authorization_signature(cmd, url, hdrs)
|
215
|
+
- base_url = url.index('?') ? url[0..(url.index('?')-1)] : url
|
216
|
+
- to_sign = "#{cmd}\n\n#{hdrs['Content-type']}\n#{hdrs['Date']}\n#{base_url}"
|
217
|
+
- # puts to_sign
|
218
|
+
- signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), secret_access_key, to_sign)).strip
|
219
|
+
- return "AWS #{access_key_id}:#{signature}"
|
220
|
+
- end
|
221
|
+
-
|
222
|
+
- def http_request_factory(cmd, url, headers, body)
|
223
|
+
- case cmd
|
224
|
+
- when 'GET' then Net::HTTP::Get.new(url,headers)
|
225
|
+
- when 'DELETE' then Net::HTTP::Delete.new(url,headers)
|
226
|
+
- when 'POST'
|
227
|
+
- req = Net::HTTP::Post.new(url,headers)
|
228
|
+
- req.body=body
|
229
|
+
- req
|
230
|
+
- when 'PUT'
|
231
|
+
- req = Net::HTTP::Put.new(url,headers)
|
232
|
+
- req.body=body
|
233
|
+
- req
|
234
|
+
- else raise 'Unsupported http request type'
|
235
|
+
- end
|
236
|
+
- end
|
237
|
+
-
|
238
|
+
- def check_errors(response)
|
239
|
+
- raise response.errors if response.errors?
|
240
|
+
- response
|
241
|
+
- end
|
242
|
+
-
|
243
|
+
- def validate_queue_name qn
|
244
|
+
- raise "Queue name, #{qn}, must be between #{QUEUE_NAME.min} and #{QUEUE_NAME.max} characters." unless QUEUE_NAME.include?(qn.length)
|
245
|
+
- raise "Queue name, #{qn}, must be alphanumeric only." if (qn =~ /\W/ )
|
246
|
+
- end
|
247
|
+
-
|
248
|
+
- def validate_queue qn, exists=true
|
249
|
+
- if exists
|
250
|
+
- raise "Never heard of queue, can't use it: #{qn.name}" unless queues.has_key? qn.name
|
251
|
+
- else
|
252
|
+
- raise "Queue already exists: #{qn}" if queues.has_key? qn.name
|
253
|
+
- end
|
254
|
+
- end
|
255
|
+
-
|
256
|
+
- def validate_message m
|
257
|
+
- raise "Message cannot be nil." if m.nil?
|
258
|
+
- raise "Message length, #{m.length}, must be between #{MESSAGE_SIZE.min} and #{MESSAGE_SIZE.max}." unless MESSAGE_SIZE.include?(m.length)
|
259
|
+
- end
|
260
|
+
-
|
261
|
+
- def validate_timeout to
|
262
|
+
- raise "Timeout, #{to}, must be between #{VISIBILITY_TIMEOUT.min} and #{VISIBILITY_TIMEOUT.max}." unless VISIBILITY_TIMEOUT.include?(to)
|
263
|
+
- end
|
264
|
+
-
|
265
|
+
- def validate_number_of_messages nom
|
266
|
+
- raise "Number of messages, #{nom}, must be between #{NUMBER_OF_MESSAGES.min} and #{NUMBER_OF_MESSAGES.max}." unless NUMBER_OF_MESSAGES.include?(nom)
|
267
|
+
- end
|
268
|
+
-
|
269
|
+
- end
|
270
|
+
-
|
271
|
+
- class SQSResponse
|
272
|
+
- attr_accessor :headers, :doc, :http_response
|
273
|
+
-
|
274
|
+
- def initialize response
|
275
|
+
- @http_response = response
|
276
|
+
- @headers = response.to_hash()
|
277
|
+
- @doc = REXML::Document.new(response.body) if response.kind_of?(Net::HTTPSuccess)
|
278
|
+
- end
|
279
|
+
-
|
280
|
+
- def message_type
|
281
|
+
- return doc ? doc.root.name : ''
|
282
|
+
- end
|
283
|
+
-
|
284
|
+
- def errors?
|
285
|
+
- (not http_response.kind_of?(Net::HTTPSuccess)) or (message_type == "Response")
|
286
|
+
- end
|
287
|
+
-
|
288
|
+
- def errors
|
289
|
+
- msg = ""
|
290
|
+
- if http_response.kind_of?(Net::HTTPSuccess)
|
291
|
+
- msg = "Errors: "
|
292
|
+
- each_node('/Response/Errors/Error') { |n|
|
293
|
+
- c = n.elements['Code'].text
|
294
|
+
- m = n.elements['Message'].text
|
295
|
+
- msg << ", " if msg != "Errors: "
|
296
|
+
- msg << "#{c} : #{m}"
|
297
|
+
- }
|
298
|
+
- else
|
299
|
+
- msg = "HTTP Error: #{http_response.code} : #{http_response.message}"
|
300
|
+
- end
|
301
|
+
- return msg
|
302
|
+
- end
|
303
|
+
-
|
304
|
+
- def get_text(xpath,default='')
|
305
|
+
- e = REXML::XPath.first( doc, xpath)
|
306
|
+
- e.nil? ? default : e.text
|
307
|
+
- end
|
308
|
+
-
|
309
|
+
- def each_node(xp)
|
310
|
+
- REXML::XPath.each(doc.root, xp) {|n| yield n}
|
311
|
+
- end
|
312
|
+
-
|
313
|
+
- def nodes(xp)
|
314
|
+
- doc.elements.to_a(xp)
|
315
|
+
- end
|
316
|
+
- end
|
317
|
+
-
|
318
|
+
- class Queue
|
319
|
+
- attr_accessor :name, :pathinfo, :domain, :visibility_timeout
|
320
|
+
-
|
321
|
+
- def self.from_url url
|
322
|
+
- return Queue.new($3,$2,$1) if url =~ /^http:\/\/(.+)\/(.+)\/(\w+)$/
|
323
|
+
- raise "Bad Queue URL: #{url}"
|
324
|
+
- end
|
325
|
+
-
|
326
|
+
- def queue_url
|
327
|
+
- "/#{pathinfo}/#{name}"
|
328
|
+
- end
|
329
|
+
-
|
330
|
+
- def initialize name, pathinfo, domain, vt=nil
|
331
|
+
- @name, @pathinfo, @domain, @visibility_timeout = name, pathinfo, domain, vt
|
332
|
+
- end
|
333
|
+
-
|
334
|
+
- def to_s
|
335
|
+
- "<AmazonSQS::Queue name='#{name}' url='#{queue_url}' domain='#{domain}'>"
|
336
|
+
- end
|
337
|
+
- end
|
338
|
+
-
|
339
|
+
- # based on stomp message, has pointer to the SQSResponseObject
|
340
|
+
- class Message
|
341
|
+
- attr_accessor :headers, :id, :body, :command, :response, :queue
|
342
|
+
-
|
343
|
+
- def self.from_element e, response, queue
|
344
|
+
- Message.new(response.headers, e.elements['MessageId'].text, e.elements['MessageBody'].text, response, queue)
|
345
|
+
- end
|
346
|
+
-
|
347
|
+
- def initialize headers, id, body, response, queue, command='MESSAGE'
|
348
|
+
- @headers, @id, @body, @response, @queue, @command = headers, id, body, response, queue, command
|
349
|
+
- headers['destination'] = queue.name
|
350
|
+
- end
|
351
|
+
-
|
352
|
+
- def to_s
|
353
|
+
- "<AmazonSQS::Message id='#{id}' body='#{body}' headers='#{headers.inspect}' command='#{command}' response='#{response}'>"
|
354
|
+
- end
|
355
|
+
- end
|
356
|
+
-
|
357
|
+
- end
|
358
|
+
- end
|
359
|
+
-end
|
360
|
+
|
361
|
+
Index: jms.rb
|
362
|
+
===================================================================
|
363
|
+
--- jms.rb (revision 107)
|
364
|
+
+++ jms.rb (working copy)
|
365
|
+
@@ -4,7 +4,7 @@
|
366
|
+
include Java
|
367
|
+
|
368
|
+
import javax.naming.InitialContext
|
369
|
+
-import javax.jms.MessageListener
|
370
|
+
+ import javax.jms.MessageListener
|
371
|
+
|
372
|
+
module ActiveMessaging
|
373
|
+
module Adapters
|
374
|
+
Index: new.rb
|
375
|
+
===================================================================
|
376
|
+
--- new.rb (revision 0)
|
377
|
+
+++ new.rb (revision 0)
|
378
|
+
@@ -0,0 +1 @@
|
379
|
+
+""
|