mohiam-babylon 0.1.7

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