seamless 1.0.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.
Files changed (57) hide show
  1. data/bin/seamless +36 -0
  2. data/lib/seamless.rb +70 -0
  3. data/lib/seamless/active_record_extension.rb +11 -0
  4. data/lib/seamless/dispatcher.rb +45 -0
  5. data/lib/seamless/message_broker.rb +12 -0
  6. data/lib/seamless/message_broker_controller.rb +11 -0
  7. data/lib/seamless/messagebroker/inmemory_message_broker.rb +45 -0
  8. data/lib/seamless/migration.rb +8 -0
  9. data/lib/seamless/model.rb +94 -0
  10. data/lib/seamless/service.rb +186 -0
  11. data/seamless/seamless_generator.rb +181 -0
  12. data/seamless/templates/README +24 -0
  13. data/seamless/templates/admin.html +166 -0
  14. data/seamless/templates/application.rb +7 -0
  15. data/seamless/templates/css/global.css +333 -0
  16. data/seamless/templates/css/seamless-admin.css +281 -0
  17. data/seamless/templates/css/seamless.css +632 -0
  18. data/seamless/templates/environment.rb +65 -0
  19. data/seamless/templates/generate +25 -0
  20. data/seamless/templates/images/arch.png +0 -0
  21. data/seamless/templates/images/arrow_undo.png +0 -0
  22. data/seamless/templates/images/cell_phone.png +0 -0
  23. data/seamless/templates/images/confirm.png +0 -0
  24. data/seamless/templates/images/deny.png +0 -0
  25. data/seamless/templates/images/dialog_close.gif +0 -0
  26. data/seamless/templates/images/dialog_close_hover.gif +0 -0
  27. data/seamless/templates/images/email.png +0 -0
  28. data/seamless/templates/images/exclamation.png +0 -0
  29. data/seamless/templates/images/indicator.gif +0 -0
  30. data/seamless/templates/images/indicator2.gif +0 -0
  31. data/seamless/templates/images/seamless.gif +0 -0
  32. data/seamless/templates/images/shadow.gif +0 -0
  33. data/seamless/templates/images/shadowAlpha.png +0 -0
  34. data/seamless/templates/images/small_star.png +0 -0
  35. data/seamless/templates/images/table_add.png +0 -0
  36. data/seamless/templates/images/table_delete.png +0 -0
  37. data/seamless/templates/images/table_edit.png +0 -0
  38. data/seamless/templates/images/table_go.png +0 -0
  39. data/seamless/templates/images/table_refresh.png +0 -0
  40. data/seamless/templates/images/tag.png +0 -0
  41. data/seamless/templates/images/warning.png +0 -0
  42. data/seamless/templates/images/wizard.gif +0 -0
  43. data/seamless/templates/images/work_phone.png +0 -0
  44. data/seamless/templates/index.html +171 -0
  45. data/seamless/templates/js/seamless-admin.js +255 -0
  46. data/seamless/templates/js/seamless.js +2755 -0
  47. data/seamless/templates/message_broker.rb +6 -0
  48. data/seamless/templates/message_broker_helper.rb +2 -0
  49. data/seamless/templates/robots.txt +2 -0
  50. data/seamless/templates/routes.rb +18 -0
  51. data/seamless/templates/seamless.xml +5 -0
  52. data/seamless/templates/server +4 -0
  53. data/seamless/templates/test_service.rb +13 -0
  54. data/service/USAGE +21 -0
  55. data/service/service_generator.rb +59 -0
  56. data/service/templates/service.rb +14 -0
  57. metadata +122 -0
data/bin/seamless ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ freeze = ARGV.any? { |option| %w(--freeze -f).include?(option) }
4
+ app_path = ARGV.first
5
+
6
+ require 'rubygems'
7
+ gem 'rails', '>= 1.2.3'
8
+
9
+ # find the path to RAILS
10
+ rails_gem = Gem.cache.search('rails').last
11
+ RAILS = rails_gem.full_gem_path
12
+
13
+ require RAILS + '/lib/ruby_version_check'
14
+ Signal.trap("INT") { puts; exit }
15
+
16
+ require RAILS + '/lib/rails/version'
17
+ if %w(--version -v).include? ARGV.first
18
+ puts "Rails #{Rails::VERSION::STRING}"
19
+ exit(0)
20
+ end
21
+
22
+
23
+ require RAILS + '/lib/rails_generator'
24
+ require 'rails_generator/scripts/generate'
25
+
26
+ Rails::Generator::Base.use_application_sources!
27
+
28
+ path = File.dirname(__FILE__) + '/../'
29
+
30
+ Rails::Generator::Base.sources << Rails::Generator::PathSource.new('', path)
31
+
32
+ # execute the seamless generator
33
+ Rails::Generator::Scripts::Generate.new.run(ARGV, {:generator => 'seamless', :source => path + 'seamless', :source_root => path + 'seamless', 'railspath'=>'../../rails-'+rails_gem.version.to_s})
34
+
35
+ Dir.chdir(app_path) { `rake rails:freeze:gems`; puts "froze" } if freeze
36
+
data/lib/seamless.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'json'
2
+ require 'action_controller'
3
+ require 'active_record'
4
+ require 'md5'
5
+
6
+ unless defined?(SEAMLESS_VERSION)
7
+
8
+ SEAMLESS_VERSION = '1.0.RC4.127'
9
+ SEAMLESS_SERVICES = Array.new
10
+
11
+ def model_name(name)
12
+ name.to_s.gsub(/[::]{2}/,'.').split('.').collect {|token| token.underscore }.join('.')
13
+ end
14
+
15
+ def silence_warnings
16
+ old_verbose, $VERBOSE = $VERBOSE, nil
17
+ yield
18
+ ensure
19
+ $VERBOSE = old_verbose
20
+ end
21
+
22
+ #
23
+ # OK, I realize this might be a little bad, but we include a few dependencies
24
+ # which print out stupid warnings on boot - we're going to suppress that
25
+ #
26
+ silence_warnings do
27
+ #
28
+ # require dependencies
29
+ #
30
+ Dir[File.dirname(__FILE__)+'/seamless/*.rb'].sort.each do |file|
31
+ require file[0..-4]
32
+ end
33
+ end
34
+
35
+ #
36
+ # trick to allow changing ActiveRecord logging stream
37
+ #
38
+ def log_to(stream)
39
+ ActiveRecord::Base.logger = Logger.new(stream)
40
+ ActiveRecord::Base.clear_active_connections!
41
+ end
42
+
43
+ #
44
+ # shortcut to turn on logging to stdout
45
+ #
46
+ def log_to_stdout
47
+ log_to STDOUT
48
+ end
49
+
50
+ #
51
+ # load any services
52
+ #
53
+ Dir[RAILS_ROOT + '/app/services/*_service.rb'].each do |file|
54
+ require file[0..-4]
55
+ name = Inflector.camelize(File.basename(file).chomp('_service.rb'))
56
+ klass = eval(name + 'Service')
57
+ mh = klass.instance
58
+ end
59
+
60
+ #
61
+ # register a message broker listener for admin seamless models
62
+ #
63
+ sam_proc = Proc.new do |req,type,obj|
64
+ resp = {'success'=>true, 'models'=> SEAMLESS_SERVICES}
65
+ Seamless::Dispatcher.instance.outgoing(req,'seamless.admin.models.response',resp)
66
+ end
67
+ Seamless::MessageBroker.register_listener('seamless.admin.models.request',sam_proc)
68
+
69
+ puts "=> Seam(less) on Rails #{SEAMLESS_VERSION}"
70
+ end
@@ -0,0 +1,11 @@
1
+ class ActiveRecord::Base
2
+ # Converts the ActiveRecord object to a JSON string. Only columns with simple
3
+ # types are included in this object so no recursion problems can occur.
4
+ def to_json(*a)
5
+ result = Hash.new
6
+ self.class.columns.each do |column|
7
+ result[column.name.to_sym] = self.send(column.name)
8
+ end
9
+ result.to_json(*a)
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module Seamless
2
+ class Dispatcher
3
+ include Singleton
4
+
5
+ def serialize(session,body)
6
+ queue = session['_seamless_mq']
7
+ body << '<?xml version="1.0"?>'
8
+ body << "<messages version='1.0' sessionid='#{session.session_id}'>"
9
+ if queue
10
+ queue.each do |msg|
11
+ body << msg
12
+ end
13
+ session['_seamless_mq']=[]
14
+ end
15
+ body << '</messages>'
16
+ end
17
+
18
+ def outgoing(obj,type,message)
19
+ queue = obj['session']['_seamless_mq']
20
+ if !queue
21
+ queue = [];
22
+ obj['session']['_seamless_mq'] = queue
23
+ end
24
+ #TODO requestid
25
+ queue << "<message requestid='' direction='OUTGOING' datatype='JSON' type='#{type}'><![CDATA["
26
+ queue << message.to_json
27
+ queue << ']]></message>'
28
+ end
29
+
30
+ def incoming(session,request,response)
31
+ body = request.env['RAW_POST_DATA']
32
+ if body
33
+ node = REXML::Document.new(body)
34
+ node.root.each_element('//message') do |message|
35
+ requestid = message.attributes['requestid']
36
+ type = message.attributes['type']
37
+ text = message.text
38
+ msg = JSON.parse(text)
39
+ req = {'requestid'=>requestid, 'session'=>session}
40
+ MessageBroker.send(req,type,msg)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ #
2
+ # require dependencies
3
+ #
4
+ Dir[File.dirname(__FILE__)+'/messagebroker/*.rb'].sort.each do |file|
5
+ require file[0..-4]
6
+ end
7
+
8
+
9
+ #
10
+ # set the default message broker to the basic in memory message broker
11
+ #
12
+ Seamless::MessageBroker = Seamless::InmemoryMessageBroker.instance
@@ -0,0 +1,11 @@
1
+
2
+ class MessageBrokerController < ActionController::Base
3
+ def process(request, response)
4
+ response.headers['Content-Type'] = 'text/xml'
5
+ session = request.session
6
+ Seamless::Dispatcher.instance.incoming(session,request,response)
7
+ Seamless::Dispatcher.instance.serialize(session,response.body)
8
+ session.close
9
+ response
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ #
2
+ # implementation of the message broker which only dispatches
3
+ # messages to internally registered listeners
4
+ #
5
+ #
6
+ module Seamless
7
+ class InmemoryMessageBroker
8
+ include Singleton
9
+
10
+ def initialize
11
+ @listeners={}
12
+ end
13
+
14
+ def register_listener(type,listener)
15
+ array = @listeners[type]
16
+ if array==nil
17
+ array=[]
18
+ @listeners[type]=array
19
+ end
20
+ array << listener
21
+ end
22
+
23
+ def unregister_listener(type,listener)
24
+ array = @listeners[type]
25
+ array.delete_if { |a| a == listener } if array
26
+ end
27
+
28
+
29
+ def send (req,type,obj)
30
+ array = @listeners[type]
31
+ if array
32
+ array.each do |listener|
33
+ begin
34
+ listener.call(req,type,obj)
35
+ rescue => error
36
+ puts $!
37
+ raise error
38
+ end
39
+ end
40
+ return true
41
+ end
42
+ false
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ module ActiveRecord
2
+ class Migration
3
+ def self.add_foreign_key(from_table, from_column, to_table)
4
+ constraint_name = "fk_#{from_table}_#{from_column}"
5
+ execute %{ALTER TABLE #{from_table} ADD CONSTRAINT #{constraint_name} FOREIGN KEY (#{from_column}) REFERENCES #{to_table}(id)}
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,94 @@
1
+ begin
2
+ require 'cached_model'
3
+ end
4
+
5
+ module Seamless
6
+ class Model < ActiveRecord::Base
7
+ self.abstract_class = true
8
+
9
+ def Model.models
10
+ SEAMLESS_MODELS
11
+ end
12
+
13
+ def update
14
+ super
15
+ delete_cache if defined?(Cache)
16
+ end
17
+
18
+ def destroy
19
+ super
20
+ delete_cache if defined?(Cache)
21
+ end
22
+
23
+ private
24
+ def delete_cache
25
+ # delete the PK entry
26
+ Cache.delete "#{self.class.name}:id:#{quoted_id}"
27
+ end
28
+
29
+ class << self
30
+
31
+ def log(msg)
32
+ if logger && logger.level == Logger::DEBUG
33
+ logger.add(Logger::DEBUG, "Seamless::Model - #{msg}")
34
+ end
35
+ end
36
+
37
+ def find_by_sql(*args)
38
+
39
+ return super(args) if not defined?(Cache)
40
+ return super(args) unless CachedModel.use_memcache? or CachedModel.use_local_cache?
41
+
42
+ # don't cache result of :all
43
+ return super(args) if args.first =~ /^SELECT \* FROM #{table_name}$/
44
+
45
+ # don't do more complex finds
46
+ case args.first
47
+ when Array then
48
+ return super(args)
49
+ end
50
+
51
+ # some SQL ends with space
52
+ sql = args.first.chomp!(" ")
53
+ log "SQL: #{sql}"
54
+
55
+ if @model_regex==nil
56
+ @model_regex = /^SELECT \* FROM #{table_name} WHERE \(#{table_name}\.`#{primary_key}` = (\d+)\)$/
57
+ end
58
+
59
+ # see if this is a single query for the primary key
60
+ match = @model_regex.match(sql)
61
+ pk = nil
62
+
63
+ if match
64
+ # extract the primary key
65
+ pk = match[1]
66
+
67
+ # check to see if the record is in the DB with PK
68
+ if CachedModel.use_memcache?
69
+ result = Cache.get "#{name}:id:#{pk}"
70
+ if result
71
+ log "cache hit: #{name}.#{primary_key} -> #{pk}"
72
+ result.instance_variable_set(:@new_record,false)
73
+ return [result]
74
+ else
75
+ log "cache miss: #{name}.#{primary_key} -> #{pk}"
76
+ end
77
+ end
78
+ end
79
+
80
+ records = super(args)
81
+
82
+ if CachedModel.use_memcache?
83
+ # store in cache
84
+ if pk
85
+ Cache.put "#{name}:id:#{pk}", records.first, CachedModel.ttl
86
+ log "cache set: #{name}.#{primary_key} -> #{pk}"
87
+ end
88
+ end
89
+
90
+ records
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,186 @@
1
+
2
+ module Seamless
3
+ class Service
4
+ include Singleton
5
+
6
+ def Service.handles_message(messagetype,handler,responsetype)
7
+ name = messagetype.gsub(/[:]|[\.\s-]/,'_')
8
+ self.class_eval <<-end_eval
9
+ alias_method :_initialize_#{name}, :initialize
10
+ def initialize
11
+ _initialize_#{name}
12
+ register("#{messagetype}","#{handler}","#{responsetype}")
13
+ end
14
+ end_eval
15
+ end
16
+
17
+ def Service.service_scaffold(model)
18
+ servicename = model_name(model)
19
+ self.class_eval <<-end_eval
20
+ alias_method :_initialize_#{name}, :initialize
21
+ def initialize
22
+ _initialize_#{name}
23
+ @model_class = eval("#{model.to_s.camelize}")
24
+ %w( create retrieve update delete list assembly search).each do |operation|
25
+ messagetype = "seamless.#{servicename}." + operation
26
+ request = messagetype + ".request"
27
+ response = messagetype + ".response"
28
+ register(request,operation,response)
29
+ SEAMLESS_SERVICES << {'name'=>name, 'model'=>servicename}
30
+ end
31
+ end
32
+
33
+ def assembly(request,message)
34
+ if self.respond_to?(:before_assembly)
35
+ request,message = send(:before_assembly,request,message)
36
+ end
37
+ data = Array.new
38
+ @model_class.columns.each do |column|
39
+ data << {'name'=>column.name,'type'=>column.type, 'limit'=>column.limit,
40
+ 'nullable'=>column.null, 'primary'=>column.primary,
41
+ 'default'=>column.default}
42
+ end
43
+ response = {'success'=>true,'columns'=>data}
44
+ if self.respond_to?(:after_assembly)
45
+ response = send(:after_assembly,response)
46
+ end
47
+ response
48
+ end
49
+
50
+ def create(request,message)
51
+ if self.respond_to?(:before_create)
52
+ request,message = send(:before_create,request,message)
53
+ end
54
+ o = @model_class.new(message)
55
+ o.save
56
+ response = {'success'=>true,'id'=>o.id}
57
+ if self.respond_to?(:after_create)
58
+ response = send(:after_create,response)
59
+ end
60
+ response
61
+ end
62
+
63
+ def retrieve(request,message)
64
+ if self.respond_to?(:before_retrieve)
65
+ request,message = send(:before_retrieve,request,message)
66
+ end
67
+ o = @model_class.find(message['id'])
68
+ response = {'success'=>true}
69
+ o.attributes.each do |key,val|
70
+ response[key]=val
71
+ end
72
+ response
73
+ if self.respond_to?(:after_retrieve)
74
+ response = send(:after_retrieve,response)
75
+ end
76
+ response
77
+ end
78
+
79
+ def update(request,message)
80
+ if self.respond_to?(:before_update)
81
+ request,message = send(:before_update,request,message)
82
+ end
83
+ o = @model_class.find(message['id'])
84
+ return {'success'=>false,'message'=>'record does not exist'} unless o
85
+ message.each do |key,value|
86
+ if o.attributes.has_key?(key)
87
+ o[key]=true if value=='true'
88
+ o[key]=false if value=='false'
89
+ o[key]=value if o[key]!=true && o[key]!=false
90
+ end
91
+ end
92
+ o.save!
93
+ response = {'success'=>true,'id'=>o.id}
94
+ if self.respond_to?(:after_update)
95
+ response = send(:after_update,response)
96
+ end
97
+ response
98
+ end
99
+
100
+ def delete(request,message)
101
+ if self.respond_to?(:before_delete)
102
+ request,message = send(:before_delete,request,message)
103
+ end
104
+ @model_class.delete(message['id'])
105
+ response = {'success'=>true,'id'=>message['id']}
106
+ if self.respond_to?(:after_delete)
107
+ response = send(:after_delete,response)
108
+ end
109
+ response
110
+ end
111
+
112
+ def search(request,message)
113
+ if self.respond_to?(:before_search)
114
+ request,message = send(:before_search,request,message)
115
+ end
116
+ query = '%%' + message['query'] + '%%'
117
+ conditions_str = Array.new
118
+ @model_class.content_columns.each do |col|
119
+ name = col.name + ' like ?'
120
+ conditions_str << name if col.type == :string
121
+ end
122
+
123
+ conditions = Array.new
124
+ conditions << conditions_str.join(' OR ')
125
+ conditions_str.length.times {|e| conditions<<query}
126
+
127
+ results = @model_class.find(:all,:conditions=>conditions)
128
+ response = {'success'=>true, 'rows'=>results, 'total'=>@model_class.count, 'count'=>results.length}
129
+
130
+ if self.respond_to?(:after_search)
131
+ response = send(:after_search,response)
132
+ end
133
+ response
134
+ end
135
+
136
+ def list(request,message)
137
+ pk = message['id']
138
+ limit = message['limit'] || 100
139
+ if pk
140
+ objs = [@model_class.find_by_id(pk)]
141
+ else
142
+ objs = @model_class.find(:all,:limit=>limit)
143
+ end
144
+ response = {'success'=>true,'rows'=>objs, 'total'=>@model_class.count}
145
+ if self.respond_to?(:after_list)
146
+ response = send(:after_list,response)
147
+ end
148
+ response
149
+ end
150
+
151
+ end_eval
152
+ end
153
+
154
+ private
155
+
156
+ def register(type,methodname,responsetype = nil)
157
+ handler = self.method(methodname)
158
+ args = handler.arity
159
+ p = Proc.new do |req,type,obj|
160
+
161
+ #
162
+ # try and be smart about sending in the request to the
163
+ # service based on the arguments he supports
164
+ #
165
+ case args
166
+ when 2
167
+ resp = handler.call(req,obj)
168
+ when 3
169
+ resp = handler.call(req,obj,req['session'])
170
+ when 4
171
+ resp = handler.call(req,obj,req['session'],req['session']['username'])
172
+ end
173
+
174
+ if responsetype
175
+ Dispatcher.instance.outgoing(req,responsetype,resp||{})
176
+ end
177
+ end
178
+ MessageBroker.register_listener(type,p)
179
+ end
180
+
181
+ def send_message(req,type,message)
182
+ MessageBroker.send(req,type,message)
183
+ end
184
+
185
+ end
186
+ end