conflict 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
+""
|