mohiam-babylon 0.1.7

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.
Files changed (47) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +111 -0
  3. data/Rakefile +145 -0
  4. data/bin/babylon +6 -0
  5. data/lib/babylon.rb +119 -0
  6. data/lib/babylon/base/controller.rb +116 -0
  7. data/lib/babylon/base/stanza.rb +23 -0
  8. data/lib/babylon/base/view.rb +49 -0
  9. data/lib/babylon/client_connection.rb +210 -0
  10. data/lib/babylon/component_connection.rb +87 -0
  11. data/lib/babylon/generator.rb +139 -0
  12. data/lib/babylon/router.rb +103 -0
  13. data/lib/babylon/router/dsl.rb +61 -0
  14. data/lib/babylon/runner.rb +148 -0
  15. data/lib/babylon/xmpp_connection.rb +172 -0
  16. data/lib/babylon/xmpp_parser.rb +111 -0
  17. data/lib/babylon/xpath_helper.rb +13 -0
  18. data/spec/bin/babylon_spec.rb +0 -0
  19. data/spec/em_mock.rb +42 -0
  20. data/spec/lib/babylon/base/controller_spec.rb +205 -0
  21. data/spec/lib/babylon/base/stanza_spec.rb +15 -0
  22. data/spec/lib/babylon/base/view_spec.rb +86 -0
  23. data/spec/lib/babylon/client_connection_spec.rb +304 -0
  24. data/spec/lib/babylon/component_connection_spec.rb +135 -0
  25. data/spec/lib/babylon/generator_spec.rb +10 -0
  26. data/spec/lib/babylon/router/dsl_spec.rb +72 -0
  27. data/spec/lib/babylon/router_spec.rb +189 -0
  28. data/spec/lib/babylon/runner_spec.rb +213 -0
  29. data/spec/lib/babylon/xmpp_connection_spec.rb +197 -0
  30. data/spec/lib/babylon/xmpp_parser_spec.rb +275 -0
  31. data/spec/lib/babylon/xpath_helper_spec.rb +25 -0
  32. data/spec/spec_helper.rb +34 -0
  33. data/templates/babylon/app/controllers/controller.rb +7 -0
  34. data/templates/babylon/app/stanzas/stanza.rb +6 -0
  35. data/templates/babylon/app/views/view.rb +6 -0
  36. data/templates/babylon/config/boot.rb +16 -0
  37. data/templates/babylon/config/config.yaml +24 -0
  38. data/templates/babylon/config/dependencies.rb +1 -0
  39. data/templates/babylon/config/routes.rb +22 -0
  40. data/templates/babylon/log/development.log +0 -0
  41. data/templates/babylon/log/production.log +0 -0
  42. data/templates/babylon/log/test.log +52 -0
  43. data/templates/babylon/script/component +46 -0
  44. data/templates/babylon/tmp/pids/README +2 -0
  45. data/test/babylon_test.rb +7 -0
  46. data/test/test_helper.rb +10 -0
  47. metadata +179 -0
@@ -0,0 +1,103 @@
1
+ require File.dirname(__FILE__)+"/router/dsl"
2
+
3
+ module Babylon
4
+ ##
5
+ # Routers are in charge of sending the right stanzas to the right controllers based on user defined Routes.
6
+ # Each application can have only one!
7
+ class StanzaRouter
8
+
9
+ attr_reader :routes, :connection
10
+
11
+ def initialize
12
+ @routes = []
13
+ end
14
+
15
+ ##
16
+ # Connected is called by the XmppConnection to indicate that the XMPP connection has been established
17
+ def connected(connection)
18
+ @connection = connection
19
+ end
20
+
21
+ ##
22
+ # Look for the first matching route and calls the corresponding action for the corresponding controller.
23
+ # Sends the response on the XMPP stream/
24
+ def route(xml_stanza)
25
+ route = routes.select{ |r| r.accepts?(xml_stanza) }.first
26
+ return false unless route
27
+ execute_route(route.controller, route.action, xml_stanza)
28
+ end
29
+
30
+ ##
31
+ # Executes the route for the given xml_stanza, by instantiating the controller_name, calling action_name and sending
32
+ # the result to the connection
33
+ def execute_route(controller_name, action_name, stanza = nil)
34
+ begin
35
+ stanza_class_name = Extlib::Inflection.camelize(action_name)
36
+ Babylon.logger.debug("looking for stanza #{stanza_class_name} for #{action_name}")
37
+ stanza = Kernel.const_get(stanza_class_name).new(stanza) if stanza
38
+ Babylon.logger.info {
39
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
40
+ }
41
+ rescue NameError
42
+ Babylon.logger.info {
43
+ "ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
44
+ }
45
+ end
46
+ controller = controller_name.new(stanza)
47
+ controller.perform(action_name)
48
+ connection.send_xml(controller.evaluate)
49
+ end
50
+
51
+ ##
52
+ # Throw away all added routes from this router. Helpful for
53
+ # testing.
54
+ def purge_routes!
55
+ @routes = []
56
+ end
57
+
58
+ ##
59
+ # Run the router DSL.
60
+ def draw(&block)
61
+ dsl = Router::DSL.new
62
+ dsl.instance_eval(&block)
63
+ dsl.routes.each do |route|
64
+ raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
65
+ end
66
+ @routes += dsl.routes
67
+ sort
68
+ end
69
+
70
+ private
71
+
72
+ def sort
73
+ @routes.sort! { |r1,r2| r2.priority <=> r1.priority }
74
+ end
75
+ end
76
+
77
+ ##
78
+ # Route class which associate an XPATH match and a priority to a controller and an action
79
+ class Route
80
+
81
+ attr_accessor :priority, :controller, :action, :xpath
82
+
83
+ ##
84
+ # Creates a new route
85
+ def initialize(params)
86
+ raise("No controller given for route") unless params["controller"]
87
+ raise("No action given for route") unless params["action"]
88
+ raise("No xpath given for route") unless params["xpath"]
89
+ @priority = params["priority"] || 0
90
+ @xpath = params["xpath"]
91
+ @controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
92
+ @action = params["action"]
93
+ end
94
+
95
+ ##
96
+ # Checks that the route matches the stanzas and calls the the action on the controller.
97
+ def accepts?(stanza)
98
+ stanza.xpath(@xpath, XpathHelper.new).empty? ? false : self
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,61 @@
1
+ module Babylon
2
+ module Router
3
+
4
+ # Creates a simple DSL for stanza routing.
5
+ class DSL
6
+ attr_reader :routes
7
+
8
+ def initialize
9
+ @routes = []
10
+ end
11
+
12
+ # Match an xpath.
13
+ def xpath(path)
14
+ @routes << {"xpath" => path}
15
+ self
16
+ end
17
+
18
+ # Set the priority of the last created route.
19
+ def priority(n)
20
+ set(:priority, n)
21
+ self
22
+ end
23
+
24
+ # Match a disco_info query.
25
+ def disco_info(node = nil)
26
+ disco_for(:info, node)
27
+ end
28
+
29
+ # Match a disco_items query.
30
+ def disco_items(node = nil)
31
+ disco_for(:items, node)
32
+ end
33
+
34
+ # Map a route to a specific controller and action.
35
+ def to(params)
36
+ set(:controller, params[:controller])
37
+ set(:action, params[:action])
38
+ # We now have all the properties we really need to create a route.
39
+ route = Route.new(@routes.pop)
40
+ @routes << route
41
+ self
42
+ end
43
+
44
+ protected
45
+ # We do this magic, or crap depending on your perspective, because we don't know whether we're setting values on a
46
+ # Hash or a Route. We can't create the Route until we have a controller and action.
47
+ def set(property, value)
48
+ last = @routes.last
49
+ last[property.to_s] = value if last.is_a?(Hash)
50
+ last.send("#{property.to_s}=", value) if last.is_a?(Route)
51
+ end
52
+
53
+ def disco_for(type, node = nil)
54
+ str = "//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco##{type.to_s}')"
55
+ str += " and @node = '#{node}'" if node
56
+ str += "]"
57
+ xpath(str)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,148 @@
1
+ module Babylon
2
+
3
+ ##
4
+ # Runner is in charge of running the application.
5
+ class Runner
6
+
7
+ ##
8
+ # Prepares the Application to run.
9
+ def self.prepare(env)
10
+
11
+ config_file = nil
12
+
13
+ if env.kind_of? Hash
14
+ config_file = File.open( env[:config] ) if env[:config]
15
+ end
16
+
17
+ # Load the configuration
18
+ config_file = File.open('config/config.yaml') if config_file.nil?
19
+ Babylon.config = YAML.load(config_file)[Babylon.environment]
20
+
21
+ # Add an outputter to the logger
22
+ log_file = Log4r::RollingFileOutputter.new("#{Babylon.environment}", :filename => "log/#{Babylon.environment}.log", :trunc => false)
23
+ case Babylon.environment
24
+ when "production"
25
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
26
+ when "development"
27
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
28
+ else
29
+ log_file.formatter = Log4r::PatternFormatter.new(:pattern => "%d (#{Process.pid}) [%l] :: %m", :date_pattern => "%d/%m %H:%M")
30
+ end
31
+ Babylon.logger.add(log_file)
32
+
33
+ # Requiring all models, stanza, controllers
34
+ ['app/models/*.rb', 'app/stanzas/*.rb', 'app/controllers/*_controller.rb'].each do |dir|
35
+ Runner.require_directory(dir)
36
+ end
37
+
38
+ # Create the router
39
+ Babylon.router = Babylon::StanzaRouter.new
40
+
41
+ # Evaluate routes defined with the new DSL router.
42
+ require 'config/routes.rb'
43
+
44
+ # Caching views
45
+ Babylon.cache_views
46
+
47
+ end
48
+
49
+ ##
50
+ # Convenience method to require files in a given directory
51
+ def self.require_directory(path)
52
+ Dir.glob(path).each { |f| require f }
53
+ end
54
+
55
+ ##
56
+ # When run is called, it loads the configuration, the routes and add them into the router
57
+ # It then loads the models.
58
+ # Finally it starts the EventMachine and connect the ComponentConnection
59
+ # You can pass an additional block that will be called upon launching, when the eventmachine has been started.
60
+ def self.run(env)
61
+
62
+ if env.kind_of? Hash
63
+ Babylon.environment = env[:env]
64
+ else
65
+ Babylon.environment = env
66
+ end
67
+
68
+ # Starting the EventMachine
69
+ EventMachine.epoll
70
+ EventMachine.run do
71
+
72
+ Runner.prepare(env)
73
+
74
+ case Babylon.config["application_type"]
75
+ when "client"
76
+ Babylon::ClientConnection.connect(Babylon.config, self)
77
+ else # By default, we assume it's a component
78
+ Babylon::ComponentConnection.connect(Babylon.config, self)
79
+ end
80
+
81
+ # And finally, let's allow the application to do all it wants to do after we started the EventMachine!
82
+ yield(self) if block_given?
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Returns the list of connection observers
88
+ def self.connection_observers()
89
+ @@observers ||= Array.new
90
+ end
91
+
92
+ ##
93
+ # Adding a connection observer. These observer will receive on_connected and on_disconnected events.
94
+ def self.add_connection_observer(observer)
95
+ @@observers ||= Array.new
96
+ if observer.ancestors.include? Babylon::Base::Controller
97
+ Babylon.logger.debug {
98
+ "Added #{observer} to the list of Connection Observers"
99
+ }
100
+ @@observers.push(observer) unless @@observers.include? observer
101
+ else
102
+ Babylon.logger.error {
103
+ "Observer can only be Babylon::Base::Controller"
104
+ }
105
+ false
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Will be called by the connection class once it is connected to the server.
111
+ # It "plugs" the router and then calls on_connected on the various observers.
112
+ def self.on_connected(connection)
113
+ Babylon.router.connected(connection)
114
+ connection_observers.each do |observer|
115
+ Babylon.router.execute_route(observer, "on_connected")
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Will be called by the connection class upon disconnection.
121
+ # It stops the event loop and then calls on_disconnected on the various observers.
122
+ def self.on_disconnected()
123
+ connection_observers.each do |conn_obs|
124
+ observer = conn_obs.new
125
+ observer.on_disconnected if observer.respond_to?("on_disconnected")
126
+ end
127
+ EventMachine.stop_event_loop
128
+ end
129
+
130
+ ##
131
+ # Will be called by the connection class when it receives and parses a stanza.
132
+ def self.on_stanza(stanza)
133
+ begin
134
+ Babylon.router.route(stanza)
135
+ rescue Babylon::NotConnected
136
+ Babylon.logger.fatal {
137
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
138
+ }
139
+ EventMachine::stop_event_loop
140
+ rescue
141
+ Babylon.logger.error {
142
+ "#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
143
+ }
144
+ end
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,172 @@
1
+ module Babylon
2
+
3
+ ##
4
+ # Connection Exception
5
+ class NotConnected < StandardError; end
6
+
7
+ ##
8
+ # xml-not-well-formed Exception
9
+ class XmlNotWellFormed < StandardError; end
10
+
11
+ ##
12
+ # Error when there is no connection to the host and port.
13
+ class NoConnection < StandardError; end
14
+
15
+ ##
16
+ # Authentication Error (wrong password/jid combination). Used for Clients and Components
17
+ class AuthenticationError < StandardError; end
18
+
19
+ ##
20
+ # Raised when the application tries to send a stanza that might be rejected by the server because it's too long.
21
+ class StanzaTooBig < StandardError; end
22
+
23
+ ##
24
+ # This class is in charge of handling the network connection to the XMPP server.
25
+ class XmppConnection < EventMachine::Connection
26
+
27
+ attr_accessor :jid, :host, :port
28
+
29
+ @@max_stanza_size = 65535
30
+
31
+ ##
32
+ # Maximum Stanza size. Default is 65535
33
+ def self.max_stanza_size
34
+ @@max_stanza_size
35
+ end
36
+
37
+ ##
38
+ # Setter for Maximum Stanza size.
39
+ def self.max_stanza_size=(_size)
40
+ @@max_stanza_size = _size
41
+ end
42
+
43
+ ##
44
+ # Connects the XmppConnection to the right host with the right port.
45
+ # It passes itself (as handler) and the configuration
46
+ # This can very well be overwritten by subclasses.
47
+ def self.connect(params, handler)
48
+ Babylon.logger.debug {
49
+ "CONNECTING TO #{params["host"]}:#{params["port"]} with #{handler.inspect} as connection handler" # Very low level Logging
50
+ }
51
+ begin
52
+ EventMachine.connect(params["host"], params["port"], self, params.merge({"handler" => handler}))
53
+ rescue RuntimeError
54
+ Babylon.logger.error {
55
+ "CONNECTION ERROR : #{$!.class} => #{$!}" # Very low level Logging
56
+ }
57
+ raise NotConnected
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Called when the connection is completed.
63
+ def connection_completed
64
+ @connected = true
65
+ Babylon.logger.debug {
66
+ "CONNECTED"
67
+ } # Very low level Logging
68
+ end
69
+
70
+ ##
71
+ # Called when the connection is terminated and stops the event loop
72
+ def unbind()
73
+ @connected = false
74
+ Babylon.logger.debug {
75
+ "DISCONNECTED"
76
+ } # Very low level Logging
77
+ begin
78
+ @handler.on_disconnected() if @handler and @handler.respond_to?("on_disconnected")
79
+ rescue
80
+ Babylon.logger.error {
81
+ "on_disconnected failed : #{$!}\n#{$!.backtrace.join("\n")}"
82
+ }
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Instantiate the Handler (called internally by EventMachine)
88
+ def initialize(params = {})
89
+ @connected = false
90
+ @jid = params["jid"]
91
+ @password = params["password"]
92
+ @host = params["host"]
93
+ @port = params["port"]
94
+ @handler = params["handler"]
95
+ @buffer = ""
96
+ end
97
+
98
+ ##
99
+ # Attaches a new parser since the network connection has been established.
100
+ def post_init
101
+ @parser = XmppParser.new(method(:receive_stanza))
102
+ end
103
+
104
+ ##
105
+ # Called when a full stanza has been received and returns it to the central router to be sent to the corresponding controller.
106
+ def receive_stanza(stanza)
107
+ Babylon.logger.debug {
108
+ "PARSED : #{stanza.to_xml}"
109
+ }
110
+ # If not handled by subclass (for authentication)
111
+ case stanza.name
112
+ when "stream:error"
113
+ if !stanza.children.empty? and stanza.children.first.name == "xml-not-well-formed"
114
+ Babylon.logger.error {
115
+ "DISCONNECTED DUE TO MALFORMED STANZA"
116
+ }
117
+ raise XmlNotWellFormed
118
+ end
119
+ # In any case, we need to close the connection.
120
+ close_connection
121
+ else
122
+ begin
123
+ @handler.on_stanza(stanza) if @handler and @handler.respond_to?("on_stanza")
124
+ rescue
125
+ Babylon.logger.error {
126
+ "on_stanza failed : #{$!}\n#{$!.backtrace.join("\n")}"
127
+ }
128
+ end
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Sends the Nokogiri::XML data (after converting to string) on the stream. Eventually it displays this data for debugging purposes.
134
+ def send_xml(xml)
135
+ if xml.is_a? Nokogiri::XML::NodeSet
136
+ xml.each do |element|
137
+ send_chunk(element.to_s)
138
+ end
139
+ else
140
+ send_chunk(xml.to_s)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def send_chunk(string)
147
+ raise NotConnected unless @connected
148
+ return if string.blank?
149
+ raise StanzaTooBig, "Stanza Too Big (#{string.length} vs. #{XmppConnection.max_stanza_size})\n #{string}" if string.length > XmppConnection.max_stanza_size
150
+ Babylon.logger.debug {
151
+ "SENDING : " + string
152
+ }
153
+ send_data string
154
+ end
155
+
156
+ ##
157
+ # receive_data is called when data is received. It is then passed to the parser.
158
+ def receive_data(data)
159
+ begin
160
+ # Babylon.logger.debug {
161
+ # "RECEIVED : #{data}"
162
+ # }
163
+ @parser.push(data)
164
+ rescue
165
+ Babylon.logger.error {
166
+ "#{$!}\n#{$!.backtrace.join("\n")}"
167
+ }
168
+ end
169
+ end
170
+ end
171
+
172
+ end