ribs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,329 @@
1
+ module Ribs
2
+ # Ribs::ClassMethods contains all the methods that gets mixed in to
3
+ # a model class.
4
+ module ClassMethods
5
+ # Get the metadata for the current model
6
+ attr_reader :ribs_metadata
7
+
8
+ # Create a new instance of this model object, optionally setting
9
+ # properties based on +attrs+.
10
+ def new(attrs = {})
11
+ obj = super()
12
+ attrs.each do |k,v|
13
+ obj.send("#{k}=", v)
14
+ end
15
+ obj
16
+ end
17
+
18
+ # First creates a model object based on the values in +attrs+ and
19
+ # then saves this to the database directly.
20
+ def create(attrs = {})
21
+ val = new(attrs)
22
+ val.save
23
+ val
24
+ end
25
+
26
+ # Depending on the value of +id_or_sym+, tries to find the model
27
+ # with a specified id, or if +id_or_sym+ is <tt>:all</tt> returns
28
+ # all instances of this model.
29
+ def find(id_or_sym)
30
+ Ribs.with_session do |s|
31
+ s.find(self.ribs_metadata.persistent_class.entity_name, id_or_sym)
32
+ end
33
+ end
34
+
35
+ # Destroys the model with the id +id+.
36
+ def destroy(id)
37
+ Ribs.with_session do |s|
38
+ s.delete(find(id))
39
+ end
40
+ end
41
+
42
+
43
+ # TODO: add inspect here
44
+ end
45
+
46
+ # Ribs::InstanceMethods provides the methods that gets mixed in to
47
+ # all instances of a model object.
48
+ module InstanceMethods
49
+ # Returns the Meat instance for this instance.
50
+ def __ribs_meat
51
+ @__ribs_meat ||= Ribs::Meat.new(self)
52
+ end
53
+
54
+ # Returns an inspection based on current values of the data in the
55
+ # database.
56
+ def inspect
57
+ "#<#{self.class.name}: #{self.__ribs_meat.properties.inspect}>"
58
+ end
59
+
60
+ # Saves this instance to the database. If the instance already
61
+ # exists, it will update the database, otherwise it will create
62
+ # it.
63
+ def save
64
+ Ribs.with_session do |s|
65
+ s.save(self)
66
+ end
67
+ end
68
+
69
+ # Removes this instance from the database.
70
+ def destroy!
71
+ __ribs_meat.destroyed = true
72
+ Ribs.with_session do |s|
73
+ s.delete(self)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Collects all the meta data about a specific Ribs model
79
+ class MetaData
80
+ # The table to connect to
81
+ attr_accessor :table
82
+ # The persistent class that Hibernate uses as a definition for
83
+ # this model.
84
+ attr_accessor :persistent_class
85
+ # The Rib that defines all the mapping data
86
+ attr_accessor :rib
87
+
88
+ # Return the property instance for the given +name+.
89
+ def [](name)
90
+ self.persistent_class.get_property(name.to_s) rescue nil
91
+ end
92
+
93
+ # Return all the properties for this model.
94
+ def properties
95
+ self.persistent_class.property_iterator.to_a.inject({}) do |h, value|
96
+ h[value.name] = value
97
+ h
98
+ end
99
+ end
100
+ end
101
+
102
+ # Contains the mapping data that gets created when calling the
103
+ # {Ribs!} method.
104
+ class Rib
105
+ # List of all the columns defined
106
+ attr_reader :columns
107
+ # List of all primary keys defined
108
+ attr_reader :primary_keys
109
+ # List of all columns to avoid
110
+ attr_reader :to_avoid
111
+
112
+ # Initializes object
113
+ def initialize
114
+ @columns = { }
115
+ @primary_keys = { }
116
+ @to_avoid = []
117
+ end
118
+
119
+ # Gets or sets the table name to work with. If +name+ is nil,
120
+ # returns the table name, if not sets the table name to +name+.
121
+ def table(name = nil)
122
+ if name
123
+ @table = name
124
+ else
125
+ @table
126
+ end
127
+ end
128
+
129
+ # Adds a new column mapping for a specific column.
130
+ def col(column, property = column, options = {})
131
+ @columns[column.to_s.downcase] = [property.to_s, options]
132
+ end
133
+
134
+ # Adds a new primary key mapping for a column.
135
+ def primary_key(column, property = column, options = {})
136
+ @primary_keys[column.to_s.downcase] = property.to_s
137
+ @columns[column.to_s.downcase] = [property.to_s, options]
138
+ end
139
+
140
+ # Avoids all the provided columns
141
+ def avoid(*columns)
142
+ @to_avoid += columns.map{|s| s.to_s.downcase}
143
+ end
144
+ end
145
+
146
+ Table = org.hibernate.mapping.Table
147
+ Column = org.hibernate.mapping.Column
148
+ Property = org.hibernate.mapping.Property
149
+ SimpleValue = org.hibernate.mapping.SimpleValue
150
+
151
+ # A simple helper class that allows the Java parts of the system to
152
+ # get the Ruby class from the PersistentClass instance.
153
+ class RubyRootClass < org.hibernate.mapping.RootClass
154
+ include org.jruby.ribs.WithRubyClass
155
+
156
+ # The Ruby class
157
+ attr_accessor :ruby_class
158
+
159
+ def initialize(*args)
160
+ super
161
+ end
162
+
163
+ # Get the Ruby class. Implementation of the WithRubyClass
164
+ # interface.
165
+ def getRubyClass
166
+ @ruby_class
167
+ end
168
+ end
169
+
170
+ class << self
171
+ # Define a rib for the class +on+. If a block is given, will first
172
+ # yield an instance of Rib to it and then base the mapping
173
+ # definition on that.
174
+ #
175
+ # +options+ have several possible values:
176
+ # * <tt>:db</tt> - the database to connect this model to
177
+ #
178
+ def define_ribs(on, options = {})
179
+ rib = Rib.new
180
+ yield rib if block_given?
181
+
182
+ define_metadata_on_class on
183
+ rm = on.ribs_metadata
184
+ rm.rib = rib
185
+
186
+ db = nil
187
+ with_session(options[:db] || :default) do |s|
188
+ db = s.db
189
+ m = s.meta_data
190
+ name = rib.table || table_name_for(on.name, m)
191
+
192
+ tables = m.get_tables nil, nil, name.to_s, %w(TABLE VIEW ALIAS SYNONYM).to_java(:String)
193
+ if tables.next
194
+ table = Table.new(tables.get_string(3))
195
+ rm.table = table
196
+ c = tables.get_string(1)
197
+ table.catalog = c if c && c.length > 0
198
+ c = tables.get_string(2)
199
+ table.schema = c if c && c.length > 0
200
+
201
+ columns_rs = m.get_columns table.catalog, table.schema, table.name, nil
202
+
203
+ while columns_rs.next
204
+ c = Column.new(columns_rs.get_string(4))
205
+ c.default_value = columns_rs.get_string(13)
206
+ c.length = columns_rs.get_int(7)
207
+ c.nullable = columns_rs.get_string(18) == 'YES'
208
+ c.precision = columns_rs.get_int(10)
209
+ c.scale = columns_rs.get_int(9)
210
+ c.sql_type = columns_rs.get_string(6)
211
+ c.sql_type_code = java.lang.Integer.new(columns_rs.get_int(5))
212
+
213
+ table.add_column(c)
214
+ end
215
+ columns_rs.close rescue nil
216
+ tables.close rescue nil
217
+
218
+ pc = RubyRootClass.new
219
+ pc.ruby_class = on
220
+ pc.entity_name = on.name
221
+ pc.table = table
222
+ pc.add_tuplizer(org.hibernate.EntityMode::MAP, "org.jruby.ribs.RubyTuplizer")
223
+
224
+ rm.persistent_class = pc
225
+
226
+ table.column_iterator.each do |c|
227
+ unless rib.to_avoid.include?(c.name.downcase)
228
+ prop = Property.new
229
+ prop.persistent_class = pc
230
+ prop.name = ((v=rib.columns[c.name.downcase]) && v[0]) || c.name
231
+ val = SimpleValue.new(table)
232
+ val.add_column(c)
233
+ val.type_name = get_type_for_sql(c.sql_type, c.sql_type_code)
234
+ prop.value = val
235
+
236
+ if (!rib.primary_keys.empty? && rib.primary_keys[c.name.downcase]) || c.name.downcase == 'id'
237
+ pc.identifier_property = prop
238
+ pc.identifier = val
239
+ else
240
+ pc.add_property(prop)
241
+ end
242
+
243
+ define_meat_accessor(on, prop.name)
244
+ end
245
+ end
246
+ pc.create_primary_key
247
+ db.mappings.add_class(pc)
248
+ else
249
+ tables.close rescue nil
250
+ raise "No table found for: #{name}"
251
+ end
252
+ end
253
+
254
+ db.reset_session_factory!
255
+ end
256
+
257
+ JTypes = java.sql.Types
258
+
259
+ private
260
+ # Defines the actual accessor for a specific property on a
261
+ # class. This will define methods that use the Meat to get data.
262
+ def define_meat_accessor(clazz, name)
263
+ downcased = name.downcase
264
+ clazz.class_eval <<CODE
265
+ def #{downcased}
266
+ self.__ribs_meat[:"#{name}"]
267
+ end
268
+
269
+ def #{downcased}=(value)
270
+ self.__ribs_meat[:"#{name}"] = value
271
+ end
272
+ CODE
273
+ end
274
+
275
+ # Returns the Java type for a specific combination of a type name
276
+ # and an SQL type code
277
+ def get_type_for_sql(name, code)
278
+ case code
279
+ when JTypes::VARCHAR
280
+ "string"
281
+ when JTypes::INTEGER
282
+ "int"
283
+ when JTypes::TIME
284
+ "java.sql.Time"
285
+ when JTypes::DATE
286
+ "java.sql.Date"
287
+ when JTypes::TIMESTAMP
288
+ "java.sql.Timestamp"
289
+ when JTypes::BLOB
290
+ "java.sql.Blob"
291
+ when JTypes::CLOB
292
+ "java.sql.Clob"
293
+ when JTypes::DOUBLE
294
+ "double"
295
+ when JTypes::SMALLINT
296
+ "boolean"
297
+ when JTypes::DECIMAL
298
+ "java.math.BigDecimal"
299
+ else
300
+ $stderr.puts [name, code].inspect
301
+ nil
302
+ end
303
+ end
304
+
305
+ # Tries to figure out if a table name should be in lower case or
306
+ # upper case, and returns the table name based on that.
307
+ def table_name_for(str, metadata)
308
+ if metadata.stores_lower_case_identifiers
309
+ str.downcase
310
+ elsif metadata.stores_upper_case_identifiers
311
+ str.upcase!
312
+ else
313
+ str
314
+ end
315
+ end
316
+
317
+ # Defines the meta data information on a class, and mixes in
318
+ # ClassMethods and InstanceMethods to it.
319
+ def define_metadata_on_class(clazz)
320
+ clazz.instance_variable_set :@ribs_metadata, Ribs::MetaData.new
321
+ class << clazz
322
+ include ClassMethods
323
+ end
324
+ clazz.class_eval do
325
+ include InstanceMethods
326
+ end
327
+ end
328
+ end
329
+ end
data/lib/ribs/meat.rb ADDED
@@ -0,0 +1,33 @@
1
+ module Ribs
2
+ # The Meat is what actually includes data. This is the holder object
3
+ # for all data that comes from a database. This allows Ribs to avoid
4
+ # polluting model objects with loads of instance variables. Instead
5
+ # everything is kept in one instance of Meat. The Meat also keeps
6
+ # track of whether the object is persistent or if it's been
7
+ # destroyed. Note that Meat is very implementation dependent, and
8
+ # should not be relied upon.
9
+ class Meat
10
+ # All the data for this instance
11
+ attr_reader :properties
12
+ # Is this instance persistent yet, or not?
13
+ attr_accessor :persistent
14
+ # Has this instance been destroyed?
15
+ attr_accessor :destroyed
16
+
17
+ # +inside+ is the instance this Meat belongs to.
18
+ def initialize(inside)
19
+ @inside = inside
20
+ @properties = {}
21
+ end
22
+
23
+ # Gets the current value of a property
24
+ def [](ix)
25
+ @properties[ix]
26
+ end
27
+
28
+ # Sets the current value of a property
29
+ def []=(ix, value)
30
+ @properties[ix] = value
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,237 @@
1
+ module Ribs
2
+ # A Ribs session maps many-to-one with a Hibernate session. When
3
+ # there are no more Ribs sessions left, the Hibernate session will
4
+ # be released too.
5
+ #
6
+ # The methods in Session is not to be used outside the framework in
7
+ # most cases, since they are internal, brittle, not based on
8
+ # Hibernate in all cases, and very much subject to change. Currently
9
+ # they provide capabilities that aren't part of the framework yet,
10
+ # such as migrations and setting up test data.
11
+ #
12
+ class Session
13
+ # This error is thrown when an operation on a session is
14
+ # attempted but the internal Hibernate session has already
15
+ # been closed.
16
+ #
17
+ class NotConnectedError < StandardError;end
18
+
19
+ class << self
20
+ # Returns a session object from the database +from+. The
21
+ # available values for from is either one of the existing
22
+ # defined database names, or <tt>:default</tt>, which will give
23
+ # a session to the default database.
24
+ #
25
+ def get(from = :default)
26
+ db = case from
27
+ when :default
28
+ Ribs::DB::get
29
+ when Ribs::DB
30
+ from
31
+ else
32
+ Ribs::DB::get(from)
33
+ end
34
+ db.session
35
+ end
36
+ end
37
+
38
+ # The current database for this session
39
+ attr_reader :db
40
+
41
+ # Creates a new session that points to the provided +db+ and
42
+ # +hibernate_session+
43
+ #
44
+ def initialize(db, hibernate_session)
45
+ @db = db
46
+ @connected = true
47
+ @hibernate_session = hibernate_session
48
+ end
49
+
50
+ # Releases this session from the database. This will possibly
51
+ # result in closing the internal Hibernate session, if this is the
52
+ # last Ribs session pointing to that Hibernate session.
53
+ def release
54
+ chk_conn
55
+ @connected = false
56
+ @db.release(self)
57
+ end
58
+
59
+ # LOW LEVEL - shouldn't be used except by Ribs
60
+ def hibernate_session # :nodoc:
61
+ @hibernate_session
62
+ end
63
+
64
+ # LOW LEVEL - shouldn't be used except by Ribs
65
+ def find(entity_name, id) # :nodoc:
66
+ chk_conn
67
+ if id == :all
68
+ @hibernate_session.create_criteria(entity_name).list.to_a
69
+ else
70
+ @hibernate_session.get(entity_name, java.lang.Integer.new(id))
71
+ end
72
+ end
73
+
74
+ # LOW LEVEL - shouldn't be used except by Ribs
75
+ def save(obj) # :nodoc:
76
+ chk_conn
77
+ tx = @hibernate_session.begin_transaction
78
+ if obj.__ribs_meat.persistent
79
+ @hibernate_session.update(obj)
80
+ else
81
+ @hibernate_session.save(obj)
82
+ obj.__ribs_meat.persistent = true
83
+ end
84
+ tx.commit
85
+ obj
86
+ end
87
+
88
+ # LOW LEVEL - shouldn't be used except by Ribs
89
+ def delete(obj) # :nodoc:
90
+ chk_conn
91
+ tx = @hibernate_session.begin_transaction
92
+ @hibernate_session.delete(obj)
93
+ tx.commit
94
+ obj
95
+ end
96
+
97
+ # LOW LEVEL - shouldn't be used except by Ribs
98
+ def meta_data # :nodoc:
99
+ chk_conn
100
+ @hibernate_session.connection.meta_data
101
+ end
102
+
103
+ # LOW LEVEL - shouldn't be used except by Ribs
104
+ def ddl(string) # :nodoc:
105
+ chk_conn
106
+ execute(string)
107
+ end
108
+
109
+ # LOW LEVEL - shouldn't be used except by Ribs
110
+ def insert(template, *data) # :nodoc:
111
+ chk_conn
112
+ conn = @hibernate_session.connection
113
+ stmt = conn.prepare_statement(template)
114
+ data.each do |d|
115
+ d.each_with_index do |item, index|
116
+ if item.kind_of?(Array)
117
+ set_prepared_statement(stmt, item[0], index+1, item[1])
118
+ else
119
+ set_prepared_statement(stmt, item, index+1, nil)
120
+ end
121
+ end
122
+ stmt.execute_update
123
+ end
124
+ conn.commit
125
+ ensure
126
+ stmt.close rescue nil
127
+ end
128
+
129
+ # LOW LEVEL - shouldn't be used except by Ribs
130
+ def select(string) # :nodoc:
131
+ chk_conn
132
+ conn = @hibernate_session.connection
133
+ stmt = conn.create_statement
134
+ rs = stmt.execute_query(string)
135
+ result = []
136
+ meta = rs.meta_data
137
+ cols = meta.column_count
138
+ while rs.next
139
+ row = []
140
+ (1..cols).each do |n|
141
+ row << from_database_type(rs.get_object(n))
142
+ end
143
+ result << row
144
+ end
145
+ result
146
+ ensure
147
+ rs.close rescue nil
148
+ stmt.close rescue nil
149
+ end
150
+
151
+ private
152
+ # Raise a NotConnectedError if not connected
153
+ def chk_conn
154
+ raise NotConnectedError unless @connected
155
+ end
156
+
157
+ # Tries to turn a Java object from the database into an equivalent
158
+ # Ruby object. Currently supports:
159
+ #
160
+ # * Strings
161
+ # * Floats
162
+ # * Integers
163
+ # * nil
164
+ # * booleans
165
+ # * Dates
166
+ # * Times
167
+ # * Timestamps
168
+ # * Blobs
169
+ # * Clobs
170
+ # * BigDecimals
171
+ def from_database_type(obj)
172
+ case obj
173
+ when String, Float, Integer, NilClass, TrueClass, FalseClass
174
+ obj
175
+ when java.sql.Date, java.sql.Time, java.sql.Timestamp
176
+ Time.at(obj.time/1000)
177
+ when java.sql.Blob
178
+ String.from_java_bytes(obj.get_bytes(1,obj.length))
179
+ when java.sql.Clob
180
+ obj.get_sub_string(1, obj.length)
181
+ when java.math.BigDecimal
182
+ BigDecimal.new(obj.to_s)
183
+ else
184
+ raise "Can't find correct type to convert #{obj.inspect} into"
185
+ end
186
+ end
187
+
188
+ # Tries to set an entry on a prepared statement, based on type
189
+ def set_prepared_statement(stmt, item, index, type)
190
+ case item
191
+ when NilClass
192
+ stmt.set_object index, nil
193
+ when String
194
+ case type
195
+ when :binary
196
+ stmt.set_bytes index, item.to_java_bytes
197
+ when :text
198
+ stmt.set_string index, item
199
+ else
200
+ stmt.set_string index, item
201
+ end
202
+ when Symbol
203
+ stmt.set_string index, item.to_s
204
+ when Integer
205
+ stmt.set_int index, item
206
+ when Float
207
+ stmt.set_float index, item
208
+ when Time
209
+ case type
210
+ when :date
211
+ stmt.set_date index, item.to_java_sql_date
212
+ when :time
213
+ stmt.set_time index, item.to_java_sql_time
214
+ when :timestamp
215
+ stmt.set_timestamp index, item.to_java_sql_time_stamp
216
+ end
217
+ when BigDecimal
218
+ stmt.set_big_decimal index, java.math.BigDecimal.new(item.to_s('F'))
219
+ when TrueClass, FalseClass
220
+ stmt.set_boolean index, item
221
+ else
222
+ raise "Can't find correct type to set prepared statement for #{item.inspect}"
223
+ end
224
+ end
225
+
226
+ # Executes a SQL string against the current hibernate
227
+ # session. This should be an updating SQL string, not a query.
228
+ def execute(string)
229
+ conn = @hibernate_session.connection
230
+ stmt = conn.create_statement
231
+ stmt.execute_update(string)
232
+ conn.commit
233
+ ensure
234
+ stmt.close rescue nil
235
+ end
236
+ end
237
+ end