julien51-babylon 0.0.14 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,12 +18,15 @@ module Babylon
18
18
  # We use a "tweak" here to send only the starting tag of stream:stream
19
19
  def connection_completed
20
20
  super
21
- builder = Nokogiri::XML::Builder.new do
22
- self.send('stream:stream', {'xmlns' => "jabber:component:accept", 'xmlns:stream' => 'http://etherx.jabber.org/streams', 'to' => @context.jid}) do
23
- paste_content_here # The stream:stream element should be cut here ;)
24
- end
25
- end
26
- start, stop = builder.to_xml.split('<paste_content_here/>')
21
+ doc = Nokogiri::XML::Document.new
22
+ stream = Nokogiri::XML::Node.new("stream", doc)
23
+ stream.add_namespace(nil, stream_namespace)
24
+ stream.add_namespace("stream", "http://etherx.jabber.org/streams")
25
+ stream["to"] = jid
26
+ doc.add_child(stream)
27
+ paste_content_here= Nokogiri::XML::Node.new("paste_content_here", doc)
28
+ stream.add_child(paste_content_here)
29
+ start, stop = doc.to_xml.split('<stream:paste_content_here/>')
27
30
  send_xml(start)
28
31
  end
29
32
 
@@ -33,7 +36,7 @@ module Babylon
33
36
  def receive_stanza(stanza)
34
37
  case @state
35
38
  when :connected # Most frequent case
36
- super # Can be dispatched
39
+ super(stanza) # Can be dispatched
37
40
 
38
41
  when :wait_for_stream
39
42
  if stanza.name == "stream:stream" && stanza.attributes['id']
@@ -72,11 +75,12 @@ module Babylon
72
75
 
73
76
  def handshake(stanza)
74
77
  hash = Digest::SHA1::hexdigest(stanza.attributes['id'].content + @password)
75
- handshake = Nokogiri::XML::Node.new("handshake", stanza.document)
78
+ doc = Nokogiri::XML::Document.new
79
+ handshake = Nokogiri::XML::Node.new("handshake", doc)
80
+ doc.add_child(handshake)
76
81
  handshake.content = hash
77
82
  handshake
78
83
  end
79
84
 
80
-
81
85
  end
82
86
  end
@@ -99,7 +99,7 @@ module Babylon
99
99
  ##
100
100
  # This is a hack since Templater doesn't offer any simple way to edit files right now...
101
101
  def add_route_for_actions_in_controller(actions, controller)
102
- sentinel = "Babylon::CentralRouter.draw do"
102
+ sentinel = "Babylon.router.draw do"
103
103
  router_path = "config/routes.rb"
104
104
  actions.each do |action|
105
105
  to_inject = "xpath(\"#{action[2]}\").to(:controller => \"#{controller}\", :action => \"#{action[0]}\").priority(#{action[1]})"
@@ -1,108 +1,74 @@
1
1
  require File.dirname(__FILE__)+"/router/dsl"
2
2
 
3
3
  module Babylon
4
-
5
-
6
- ##
7
- # Undefined stanza
8
- class UndefinedStanza < Exception; end
9
-
10
4
  ##
11
- # The router is in charge of sending the right stanzas to the right controllers based on user defined Routes.
12
- module Router
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
13
8
 
14
- @@connection = nil
9
+ attr_reader :routes, :connection
15
10
 
16
- ##
17
- # Add several routes to the router
18
- # Routes should be of form {name => params}
19
- def add_routes(routes)
20
- routes.each do |name, params|
21
- add_route(Route.new(params))
22
- end
11
+ def initialize
12
+ @routes = []
23
13
  end
24
14
 
25
15
  ##
26
16
  # Connected is called by the XmppConnection to indicate that the XMPP connection has been established
27
17
  def connected(connection)
28
- @@connection = connection
29
- end
30
-
31
- ##
32
- # Accessor for @@connection
33
- def connection
34
- @@connection
18
+ @connection = connection
35
19
  end
36
20
 
37
21
  ##
38
- # Insert a route and makes sure that the routes are sorted
39
- def add_route(route)
40
- @routes ||= []
41
- @routes << route
42
- sort
43
- end
44
-
45
22
  # Look for the first matching route and calls the corresponding action for the corresponding controller.
46
23
  # Sends the response on the XMPP stream/
47
- def route(stanza)
48
- return false if !@@connection
49
- @routes ||= []
50
- @routes.each { |route|
51
- if route.accepts?(stanza)
52
- # Here should happen the magic : call the controller
53
- Babylon.logger.info("ROUTING TO #{route.controller}::#{route.action}")
54
- # Parsing the stanza
55
- begin
56
- controller = route.controller.new(Kernel.const_get(route.action.capitalize).new(stanza))
57
- rescue NameError
58
- raise UndefinedStanza
59
- end
60
- controller.perform(route.action) do |response|
61
- # Response should be a Nokogiri::Nodeset
62
- @@connection.send_xml(response)
63
- end
64
- return true
65
- end
66
- }
67
- false
68
- end
69
-
70
- # Throw away all added routes from this router. Helpful for
71
- # testing.
72
- def purge_routes!
73
- @routes = []
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, xml_stanza = nil)
34
+ Babylon.logger.info("ROUTING TO #{controller_name}::#{action_name}")
35
+ stanza = nil
36
+ stanza = Kernel.const_get(action_name.capitalize).new(xml_stanza) if xml_stanza
37
+ controller = controller_name.new(stanza)
38
+ controller.perform(action_name)
39
+ connection.send_xml(controller.evaluate)
74
40
  end
75
-
76
- # Run the router DSL.
77
- def draw(&block)
78
- r = Router::DSL.new
79
- r.instance_eval(&block)
80
- r.routes.each do |route|
81
- raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
82
- end
83
- @routes ||= []
84
- @routes += r.routes
41
+
42
+ ##
43
+ # Throw away all added routes from this router. Helpful for
44
+ # testing.
45
+ def purge_routes!
46
+ @routes = []
47
+ end
48
+
49
+ ##
50
+ # Run the router DSL.
51
+ def draw(&block)
52
+ dsl = Router::DSL.new
53
+ dsl.instance_eval(&block)
54
+ dsl.routes.each do |route|
55
+ raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
56
+ end
57
+ @routes += dsl.routes
85
58
  sort
86
59
  end
87
60
 
88
61
  private
62
+
89
63
  def sort
90
- @routes.sort! { |r1,r2|
91
- r2.priority <=> r1.priority
92
- }
64
+ @routes.sort! { |r1,r2| r2.priority <=> r1.priority }
93
65
  end
94
66
  end
95
67
 
96
- ##
97
- # Main router for a Babylon Application.
98
- module CentralRouter
99
- extend Router
100
- end
101
-
102
68
  ##
103
69
  # Route class which associate an XPATH match and a priority to a controller and an action
104
70
  class Route
105
-
71
+
106
72
  attr_accessor :priority, :controller, :action, :xpath
107
73
 
108
74
  ##
@@ -110,21 +76,17 @@ module Babylon
110
76
  def initialize(params)
111
77
  raise("No controller given for route") unless params["controller"]
112
78
  raise("No action given for route") unless params["action"]
79
+ raise("No xpath given for route") unless params["xpath"]
113
80
  @priority = params["priority"] || 0
114
- @xpath = params["xpath"] if params["xpath"]
115
- @css = params["css"] if params["css"]
81
+ @xpath = params["xpath"]
116
82
  @controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
117
83
  @action = params["action"]
118
84
  end
119
-
85
+
120
86
  ##
121
- # Checks that the route matches the stanzas and calls the the action on the controller
87
+ # Checks that the route matches the stanzas and calls the the action on the controller.
122
88
  def accepts?(stanza)
123
- if @xpath
124
- stanza.xpath(@xpath, XpathHelper.new).first ? self : false
125
- elsif @css
126
- stanza.css(@css).first ? self : false
127
- end
89
+ stanza.xpath(@xpath, XpathHelper.new).empty? ? false : self
128
90
  end
129
91
 
130
92
  end
@@ -14,12 +14,6 @@ module Babylon
14
14
  @routes << {"xpath" => path}
15
15
  self
16
16
  end
17
-
18
- # Match a css.
19
- def css(path)
20
- @routes << {"css" => path}
21
- self
22
- end
23
17
 
24
18
  # Set the priority of the last created route.
25
19
  def priority(n)
@@ -4,6 +4,38 @@ module Babylon
4
4
  # Runner is in charge of running the application.
5
5
  class Runner
6
6
 
7
+ ##
8
+ # Prepares the Application to run.
9
+ def self.prepare(env)
10
+ # Load the configuration
11
+ config_file = File.open('config/config.yaml')
12
+ Babylon.config = YAML.load(config_file)[Babylon.environment]
13
+
14
+ # Add an outputter to the logger
15
+ Babylon.logger.add(Log4r::FileOutputter.new("#{Babylon.environment}", :filename => "log/#{Babylon.environment}.log", :trunc => false))
16
+
17
+ # Requiring all models, stanza, controllers
18
+ ['app/models/*.rb', 'app/stanzas/*.rb', 'app/controllers/*_controller.rb'].each do |dir|
19
+ Runner.require_directory(dir)
20
+ end
21
+
22
+ # Create the router
23
+ Babylon.router = Babylon::StanzaRouter.new
24
+
25
+ # Evaluate routes defined with the new DSL router.
26
+ require 'config/routes.rb'
27
+
28
+ # Caching views
29
+ Babylon.cache_views
30
+
31
+ end
32
+
33
+ ##
34
+ # Convenience method to require files in a given directory
35
+ def self.require_directory(path)
36
+ Dir.glob(path).each { |f| require f }
37
+ end
38
+
7
39
  ##
8
40
  # When run is called, it loads the configuration, the routes and add them into the router
9
41
  # It then loads the models.
@@ -17,31 +49,7 @@ module Babylon
17
49
  EventMachine.epoll
18
50
  EventMachine.run do
19
51
 
20
- # Add an outputter to the logger
21
- Babylon.logger.add(Log4r::FileOutputter.new("#{Babylon.environment}", :filename => "log/#{Babylon.environment}.log", :trunc => false))
22
-
23
- # Requiring all models
24
- Dir.glob('app/models/*.rb').each { |f| require f }
25
-
26
- # Requiring all stanzas
27
- Dir.glob('app/stanzas/*.rb').each { |f| require f }
28
-
29
- # Load the controllers
30
- Dir.glob('app/controllers/*_controller.rb').each {|f| require f }
31
-
32
- # Evaluate routes defined with the new DSL router.
33
- CentralRouter.draw do
34
- eval File.read("config/routes.rb")
35
- end
36
-
37
- config_file = File.open('config/config.yaml')
38
-
39
- # Caching views in production mode.
40
- if Babylon.environment == "production"
41
- Babylon.cache_views
42
- end
43
-
44
- Babylon.config = YAML.load(config_file)[Babylon.environment]
52
+ Runner.prepare(env)
45
53
 
46
54
  case Babylon.config["application_type"]
47
55
  when "client"
@@ -63,18 +71,24 @@ module Babylon
63
71
 
64
72
  ##
65
73
  # Adding a connection observer. These observer will receive on_connected and on_disconnected events.
66
- def self.add_connection_observer(object)
67
- @@observers ||= Array.new
68
- @@observers.push(object)
74
+ def self.add_connection_observer(observer)
75
+ @@observers ||= Array.new
76
+ if observer.ancestors.include? Babylon::Base::Controller
77
+ Babylon.logger.debug("Added #{observer} to the list of Connection Observers")
78
+ @@observers.push(observer) unless @@observers.include? observer
79
+ else
80
+ Babylon.logger.error("Observer can only be Babylon::Base::Controller")
81
+ false
82
+ end
69
83
  end
70
84
 
71
85
  ##
72
86
  # Will be called by the connection class once it is connected to the server.
73
87
  # It "plugs" the router and then calls on_connected on the various observers.
74
88
  def self.on_connected(connection)
75
- Babylon::CentralRouter.connected(connection)
76
- connection_observers.each do |conn_obs|
77
- conn_obs.on_connected(connection) if conn_obs.respond_to?("on_connected")
89
+ Babylon.router.connected(connection)
90
+ connection_observers.each do |observer|
91
+ Babylon.router.execute_route(observer, "on_connected")
78
92
  end
79
93
  end
80
94
 
@@ -82,16 +96,24 @@ module Babylon
82
96
  # Will be called by the connection class upon disconnection.
83
97
  # It stops the event loop and then calls on_disconnected on the various observers.
84
98
  def self.on_disconnected()
85
- EventMachine.stop_event_loop
86
99
  connection_observers.each do |conn_obs|
87
- conn_obs.on_disconnected if conn_obs.respond_to?("on_disconnected")
100
+ observer = conn_obs.new
101
+ observer.on_disconnected if observer.respond_to?("on_disconnected")
88
102
  end
103
+ EventMachine.stop_event_loop
89
104
  end
90
105
 
91
106
  ##
92
107
  # Will be called by the connection class when it receives and parses a stanza.
93
108
  def self.on_stanza(stanza)
94
- Babylon::CentralRouter.route(stanza)
109
+ begin
110
+ Babylon.router.route(stanza)
111
+ rescue Babylon::NotConnected
112
+ Babylon.logger.fatal("#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}")
113
+ EventMachine::stop_event_loop
114
+ rescue
115
+ Babylon.logger.error("#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}")
116
+ end
95
117
  end
96
118
 
97
119
  end
@@ -1,40 +1,61 @@
1
1
  module Babylon
2
-
2
+
3
3
  ##
4
4
  # Connection Exception
5
- class NotConnected < Exception; end
5
+ class NotConnected < StandardError; end
6
6
 
7
7
  ##
8
8
  # xml-not-well-formed Exception
9
- class XmlNotWellFormed < Exception; end
9
+ class XmlNotWellFormed < StandardError; end
10
10
 
11
11
  ##
12
12
  # Error when there is no connection to the host and port.
13
- class NoConnection < Exception; end
13
+ class NoConnection < StandardError; end
14
14
 
15
15
  ##
16
16
  # Authentication Error (wrong password/jid combination). Used for Clients and Components
17
- class AuthenticationError < Exception; end
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
18
22
 
19
23
  ##
20
24
  # This class is in charge of handling the network connection to the XMPP server.
21
25
  class XmppConnection < EventMachine::Connection
22
-
26
+
23
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
24
42
 
25
43
  ##
26
- # Connects the XmppConnection to the right host with the right port. I
44
+ # Connects the XmppConnection to the right host with the right port.
27
45
  # It passes itself (as handler) and the configuration
28
46
  # This can very well be overwritten by subclasses.
29
47
  def self.connect(params, handler)
30
- Babylon.logger.debug("CONNECTING TO #{params["host"]}:#{params["port"]}") # Very low level Logging
48
+ Babylon.logger.debug("CONNECTING TO #{params["host"]}:#{params["port"]} with #{handler.inspect} as connection handler") # Very low level Logging
31
49
  begin
32
50
  EventMachine.connect(params["host"], params["port"], self, params.merge({"handler" => handler}))
33
- rescue
34
- Babylon.logger.error("CONNECTION ERROR : #{$!}") # Very low level Logging
51
+ rescue RuntimeError
52
+ Babylon.logger.error("CONNECTION ERROR : #{$!.class} => #{$!}") # Very low level Logging
53
+ raise NotConnected
35
54
  end
36
55
  end
37
56
 
57
+ ##
58
+ # Called when the connection is completed.
38
59
  def connection_completed
39
60
  @connected = true
40
61
  Babylon.logger.debug("CONNECTED") # Very low level Logging
@@ -53,17 +74,22 @@ module Babylon
53
74
  end
54
75
 
55
76
  ##
56
- # Instantiate the Handler (called internally by EventMachine) and attaches a new XmppParser
57
- def initialize(params)
58
- super()
77
+ # Instantiate the Handler (called internally by EventMachine)
78
+ def initialize(params = {})
59
79
  @connected = false
60
- @jid = params["jid"]
61
- @password = params["password"]
62
- @host = params["host"]
63
- @port = params["port"]
64
- @handler = params["handler"]
65
- @parser = XmppParser.new(&method(:receive_stanza))
80
+ @jid = params["jid"]
81
+ @password = params["password"]
82
+ @host = params["host"]
83
+ @port = params["port"]
84
+ @handler = params["handler"]
85
+ @buffer = ""
66
86
  end
87
+
88
+ ##
89
+ # Attaches a new parser since the network connection has been established.
90
+ def post_init
91
+ @parser = XmppParser.new(method(:receive_stanza))
92
+ end
67
93
 
68
94
  ##
69
95
  # Called when a full stanza has been received and returns it to the central router to be sent to the corresponding controller.
@@ -73,7 +99,6 @@ module Babylon
73
99
  case stanza.name
74
100
  when "stream:error"
75
101
  if !stanza.children.empty? and stanza.children.first.name == "xml-not-well-formed"
76
- # <stream:error><xml-not-well-formed xmlns:xmlns="urn:ietf:params:xml:ns:xmpp-streams"/></stream:error>
77
102
  Babylon.logger.error("DISCONNECTED DUE TO MALFORMED STANZA")
78
103
  raise XmlNotWellFormed
79
104
  end
@@ -89,40 +114,25 @@ module Babylon
89
114
  end
90
115
 
91
116
  ##
92
- # Sends the Nokogiri::XML data (after converting to string) on the stream. It also appends the right "from" to be the component's JId if none has been mentionned. Eventually it displays this data for debugging purposes.
93
- # This method also adds a "from" attribute to all stanza if it was ommited (the full jid) only if a "to" attribute is present. if not, we assume that we're speaking to the server and the server doesn't need a "from" to identify where the message is coming from.
117
+ # Sends the Nokogiri::XML data (after converting to string) on the stream. Eventually it displays this data for debugging purposes.
94
118
  def send_xml(xml)
95
- raise NotConnected unless @connected
96
119
  if xml.is_a? Nokogiri::XML::NodeSet
97
- xml.each do |node|
98
- send_node(node)
120
+ xml.each do |element|
121
+ send_chunk(element.to_s)
99
122
  end
100
- elsif xml.is_a? Nokogiri::XML::Node
101
- send_node(xml)
102
123
  else
103
- # We try a cast into a string.
104
- send_string("#{xml}")
124
+ send_chunk(xml.to_s)
105
125
  end
106
126
  end
107
127
 
108
128
  private
109
129
 
110
- ##
111
- # Sends a node on the "line".
112
- def send_node(node)
113
- node["from"] ||= jid if node["to"]
114
- send_string(node.to_xml)
115
- end
116
-
117
- ##
118
- # Sends a string on the line
119
- def send_string(string)
120
- begin
121
- Babylon.logger.debug("SENDING : #{string}")
122
- send_data("#{string}")
123
- rescue
124
- Babylon.logger.error("#{$!}\n#{$!.backtrace.join("\n")}")
125
- end
130
+ def send_chunk(string)
131
+ raise NotConnected unless @connected
132
+ return if string.blank?
133
+ raise StanzaTooBig if string.length > XmppConnection.max_stanza_size
134
+ Babylon.logger.debug("SENDING : " + string)
135
+ send_data string
126
136
  end
127
137
 
128
138
  ##
@@ -130,7 +140,7 @@ module Babylon
130
140
  def receive_data(data)
131
141
  begin
132
142
  Babylon.logger.debug("RECEIVED : #{data}")
133
- @parser.push(data)
143
+ @parser.push(data)
134
144
  rescue
135
145
  Babylon.logger.error("#{$!}\n#{$!.backtrace.join("\n")}")
136
146
  end