class Knj::Objects attr_reader :args, :events, :data def initialize(args) @callbacks = {} @args = Knj::ArrayExt.hash_sym(args) @args[:col_id] = :id if !@args[:col_id] @args[:class_pre] = "class_" if !@args[:class_pre] @args[:module] = Kernel if !@args[:module] @args[:cache] = :weak if !@args.key?(:cache) @objects = {} @data = {} require "weakref" if @args[:cache] == :weak @events = Knj::Event_handler.new @events.add_event( :name => :no_html, :connections_max => 1 ) @events.add_event( :name => :no_date, :connections_max => 1 ) raise "No DB given." if !@args[:db] raise "No class path given." if !@args[:class_path] and (@args[:require] or !@args.key?(:require)) if args[:require_all] loads = [] Dir.foreach(@args[:class_path]) do |file| next if file == "." or file == ".." or !file.match(/\.rb$/) file_parsed = file file_parsed.gsub!(@args[:class_pre], "") if @args.key?(:class_pre) file_parsed.gsub!(/\.rb$/, "") file_parsed = Knj::Php.ucwords(file_parsed) loads << file_parsed self.requireclass(file_parsed, {:load => false}) end loads.each do |load_class| self.load_class(load_class) end end end def init_class(classname) return false if @objects.key?(classname) @objects[classname] = {} end #Returns a cloned version of the @objects variable. Cloned because iteration on it may crash some of the other methods in Ruby 1.9+ def objects objs_cloned = {} @objects.keys.each do |key| objs_cloned[key] = @objects[key].clone end return objs_cloned end def db return @args[:db] end def count_objects count = 0 @objects.keys.each do |key| count += @objects[key].length end return count end def connect(args, &block) raise "No object given." if !args["object"] raise "No signals given." if !args.key?("signal") and !args.key?("signals") args["block"] = block if block_given? @callbacks[args["object"]] = {} if !@callbacks[args["object"]] conn_id = @callbacks[args["object"]].length.to_s @callbacks[args["object"]][conn_id] = args end def call(args, &block) classstr = args["object"].class.to_s if @callbacks.key?(classstr) @callbacks[classstr].clone.each do |callback_key, callback| docall = false if callback.key?("signal") and args.key?("signal") and callback["signal"] == args["signal"] docall = true elsif callback["signals"] and args["signal"] and callback["signals"].index(args["signal"]) != nil docall = true end next if !docall if callback["block"] callargs = [] arity = callback["block"].arity if arity <= 0 #do nothing elsif arity == 1 callargs << args["object"] else raise "Unknown number of arguments: #{arity}" end callback["block"].call(*callargs) elsif callback["callback"] Knj::Php.call_user_func(callback["callback"], args) else raise "No valid callback given." end end end end def requireclass(classname, args = {}) classname = classname.to_sym if !@objects.key?(classname) if (@args[:require] or !@args.key?(:require)) and (!args.key?(:require) or args[:require]) filename = "#{@args[:class_path]}/#{@args[:class_pre]}#{classname.to_s.downcase}.rb" filename_req = "#{@args[:class_path]}/#{@args[:class_pre]}#{classname.to_s.downcase}" raise "Class file could not be found: #{filename}." if !File.exists?(filename) require filename_req end if args[:class] classob = args[:class] else classob = @args[:module].const_get(classname) end if (classob.respond_to?(:load_columns) or classob.respond_to?(:datarow_init)) and (!args.key?(:load) or args[:load]) self.load_class(classname, args) end @objects[classname] = {} end end def load_class(classname, args = {}) if args[:class] classob = args[:class] else classob = @args[:module].const_get(classname) end pass_arg = Knj::Hash_methods.new(:ob => self, :db => @args[:db]) classob.load_columns(pass_arg) if classob.respond_to?(:load_columns) classob.datarow_init(pass_arg) if classob.respond_to?(:datarow_init) end def get(classname, data) classname = classname.to_sym if data.is_a?(Integer) or data.is_a?(String) or data.is_a?(Fixnum) id = data.to_i elsif data.is_a?(Hash) and data[@args[:col_id].to_sym] id = data[@args[:col_id].to_sym].to_i elsif data.is_a?(Hash) and data[@args[:col_id].to_s] id = data[@args[:col_id].to_s].to_i elsif raise Knj::Errors::InvalidData, "Unknown data: '#{data.class.to_s}'." end if @objects.key?(classname) and @objects[classname].key?(id) case @args[:cache] when :weak begin obj = @objects[classname][id] obj = obj.__getobj__ if obj.is_a?(WeakRef) #This actually happens sometimes... WTF!? - knj if obj.is_a?(Knj::Datarow) and obj.respond_to?(:table) and obj.respond_to?(:id) and obj.table.to_sym == classname and obj.id.to_i == id return obj else raise WeakRef::RefError end rescue WeakRef::RefError @objects[classname].delete(id) end else return @objects[classname][id] end end self.requireclass(classname) if !@objects.key?(classname) if @args[:datarow] obj = @args[:module].const_get(classname).new(Knj::Hash_methods.new(:ob => self, :data => data)) else args = [data] args = args | @args[:extra_args] if @args[:extra_args] obj = @args[:module].const_get(classname).new(*args) end case @args[:cache] when :weak @objects[classname][id] = WeakRef.new(obj) else @objects[classname][id] = obj end return obj end def object_finalizer(id) classname = @objects_idclass[id] if classname @objects[classname].delete(id) @objects_idclass.delete(id) end end def get_by(classname, args = {}) classname = classname.to_sym self.requireclass(classname) classob = @args[:module].const_get(classname) raise "list-function has not been implemented for #{classname}" if !classob.respond_to?("list") args[:limit_from] = 0 args[:limit_to] = 1 self.list(classname, args) do |obj| return obj end return false end def get_try(obj, col_name, obj_name = nil) if !obj_name if match = col_name.to_s.match(/^(.+)_id$/) obj_name = Knj::Php.ucwords(match[1]).to_sym else raise "Could not figure out objectname for: #{col_name}." end end id_data = obj[col_name].to_i return false if !id_data begin return self.get(obj_name, id_data) rescue Knj::Errors::NotFound return false end end def list(classname, args = {}, &block) classname = classname.to_sym self.requireclass(classname) classob = @args[:module].const_get(classname) raise "list-function has not been implemented for #{classname}" if !classob.respond_to?("list") if @args[:datarow] ret = classob.list(Knj::Hash_methods.new(:args => args, :ob => self, :db => @args[:db])) else realargs = [args] realargs = realargs | @args[:extra_args] if @args[:extra_args] ret = classob.list(*realargs) end if block_given? ret.each do |obj| yield(obj) end else return ret end end def list_opts(classname, args = {}) Knj::ArrayExt.hash_sym(args) classname = classname.to_sym if args[:list_args] obs = self.list(classname, args[:list_args]) else obs = self.list(classname) end html = "" if args[:addnew] or args[:add] html += "#{_("Add new")}" end obs.each do |object| html += "" end end return html end def list_optshash(classname, args = {}) Knj::ArrayExt.hash_sym(args) classname = classname.to_sym if args[:list_args] obs = self.list(classname, args[:list_args]) else obs = self.list(classname) end if RUBY_VERSION[0..2] == 1.8 and Knj::Php.class_exists("Dictionary") list = Dictionary.new else list = {} end if args[:addnew] or args[:add] list["0"] = _("Add new") elsif args[:choose] list["0"] = _("Choose") + ":" elsif args[:all] list["0"] = _("All") elsif args[:none] list["0"] = _("None") end obs.each do |object| list[object.id] = object.title end return list end # Returns a list of a specific object by running specific SQL against the database. def list_bysql(classname, sql) classname = classname.to_sym ret = [] if !block_given? @args[:db].q(sql) do |d_obs| if block_given? yield(self.get(classname, d_obs)) else ret << self.get(classname, d_obs) end end return ret if !block_given? end # Add a new object to the database and to the cache. def add(classname, data = {}) classname = classname.to_sym self.requireclass(classname) args = [data] args = args | @args[:extra_args] if @args[:extra_args] if @args[:datarow] classobj = @args[:module].const_get(classname) if classobj.respond_to?(:add) classobj.add(Knj::Hash_methods.new( :ob => self, :db => self.db, :data => data )) end required_data = classobj.required_data required_data.each do |req_data| if !data.key?(req_data[:col]) raise "No '#{req_data[:class]}' given by the data '#{req_data[:col]}'." end begin obj = self.get(req_data[:class], data[req_data[:col]]) rescue Knj::Errors::NotFound raise "The '#{req_data[:class]}' by ID '#{data[req_data[:col]]}' could not be found with the data '#{req_data[:col]}'." end end ins_id = @args[:db].insert(classname, data, {:return_id => true}) retob = self.get(classname, ins_id) else retob = @args[:module].const_get(classname).add(*args) end self.call("object" => retob, "signal" => "add") if retob.respond_to?(:add_after) retob.send(:add_after, {}) end return retob end def adds(classname, datas) if !@args[:datarow] datas.each do |data| @args[:module].const_get(classname).add(*args) self.call("object" => retob, "signal" => "add") end else if @args[:module].const_get(classname).respond_to?(:add) datas.each do |data| @args[:module].const_get(classname).add(Knj::Hash_methods.new( :ob => self, :db => self.db, :data => data )) end end db.insert_multi(classname, datas) end end def static(class_name, method_name, *args) raise "Only available with datarow enabled." if !@args[:datarow] class_name = class_name.to_sym method_name = method_name.to_sym self.requireclass(class_name) class_obj = @args[:module].const_get(class_name) raise "The class '#{class_obj.name}' has no such method: '#{method_name}'." if !class_obj.respond_to?(method_name) method_obj = class_obj.method(method_name) pass_args = [] pass_args << Knj::Hash_methods.new( :ob => self, :db => self.db ) args.each do |arg| pass_args << arg end method_obj.call(*pass_args) end # Unset object. Do this if you are sure, that there are no more references left. This will be done automatically when deleting it. def unset(object) if object.is_a?(Array) object.each do |obj| unset(obj) end return nil end classname = object.class.name if @args[:module] classname = classname.gsub(@args[:module].name + "::", "") end classname = classname.to_sym #if !@objects.key?(classname) #raise "Could not find object class in cache: #{classname}." #elsif !@objects[classname].key?(object.id.to_i) #errstr = "" #errstr += "Could not unset object from cache.\n" #errstr += "Class: #{object.class.name}.\n" #errstr += "ID: #{object.id}.\n" #errstr += "Could not find object ID in cache." #raise errstr #else @objects[classname].delete(object.id.to_i) #end end def unset_class(classname) if classname.is_a?(Array) classname.each do |classn| self.unset_class(classn) end return false end classname = classname.to_sym return false if !@objects.key?(classname) @objects[classname] = {} end # Delete an object. Both from the database and from the cache. def delete(object) self.call("object" => object, "signal" => "delete_before") self.unset(object) obj_id = object.id object.delete if object.respond_to?(:delete) if @args[:datarow] object.class.depending_data.each do |dep_data| objs = self.list(dep_data[:classname], {dep_data[:colname].to_s => object.id, "limit" => 1}) if !objs.empty? raise "Cannot delete <#{object.class.name}:#{object.id}> because <#{objs[0].class.name}:#{objs[0].id}> depends on it." end end @args[:db].delete(object.table, {:id => obj_id}) end self.call("object" => object, "signal" => "delete") object.destroy end def deletes(objs) if !@args[:datarow] objs.each do |obj| self.delete(obj) end else arr_ids = [] ids = [] objs.each do |obj| ids << obj.id if ids.length >= 1000 arr_ids << ids ids = [] end obj.delete if obj.respond_to?(:delete) end arr_ids << ids if ids.length > 0 arr_ids.each do |ids| @args[:db].delete(objs[0].table, {:id => ids}) end end end # Try to clean up objects by unsetting everything, start the garbagecollector, get all the remaining objects via ObjectSpace and set them again. Some (if not all) should be cleaned up and our cache should still be safe... dirty but works. def clean(classn) return false if @args[:cache] == :weak if classn.is_a?(Array) classn.each do |realclassn| self.clean(realclassn) end else return false if !@objects.key?(classn) @objects[classn] = {} GC.start end end def clean_all return false if @args[:cache] == :weak classnames = [] @objects.keys.each do |classn| classnames << classn end classnames.each do |classn| @objects[classn] = {} end GC.start end def clean_recover return false if @args[:cache] == :weak return false if RUBY_ENGINE == "jruby" and !JRuby.objectspace @objects.keys.each do |classn| data = @objects[classn] classobj = @args[:module].const_get(classn) ObjectSpace.each_object(classobj) do |obj| begin data[obj.id.to_i] = obj rescue => e if e.message == "No data on object." #Object has been unset - skip it. next end raise e end end end end def sqlhelper(list_args, args_def) if args[:db] db = args[:db] else db = @args[:db] end args = args_def if args[:table] table_def = "`#{db.esc_table(args[:table])}`." else table_def = "" end sql_joins = "" sql_where = "" sql_order = "" sql_limit = "" do_joins = {} limit_from = nil limit_to = nil if list_args.key?("orderby") orders = [] orderstr = list_args["orderby"] list_args["orderby"] = [list_args["orderby"]] if list_args["orderby"].is_a?(Hash) if list_args["orderby"].is_a?(String) found = false found = true if args[:cols].key?(orderstr) if found sql_order += " ORDER BY " ordermode = " ASC" if list_args.key?("ordermode") if list_args["ordermode"] == "desc" ordermode = " DESC" elsif list_args["ordermode"] == "asc" ordermode = " ASC" raise "Unknown ordermode: #{list_args["ordermode"]}" end list_args.delete("ordermode") end sql_order += "#{table_def}`#{db.esc_col(list_args["orderby"])}`#{ordermode}" list_args.delete("orderby") end elsif list_args["orderby"].is_a?(Array) sql_order += " ORDER BY " list_args["orderby"].each do |val| ordermode = nil orderstr = nil found = false if val.is_a?(Array) orderstr = val[0] if val[1] == "asc" ordermode = " ASC" elsif val[1] == "desc" ordermode = "DESC" end elsif val.is_a?(String) orderstr = val ordermode = " ASC" elsif val.is_a?(Hash) raise "No joined tables." if !args.key?(:joined_tables) if val[:mode] == "asc" ordermode = " ASC" elsif val[:mode] == "desc" ordermode = " DESC" end if args[:joined_tables] args[:joined_tables].each do |table_name, table_data| if table_name.to_s == val[:table] do_joins[table_name] = true orders << "`#{db.esc_table(table_name)}`.`#{db.esc_col(val[:col])}`#{ordermode}" found = true break end end end else raise "Unknown object: #{val.class.name}" end found = true if args[:cols].key?(orderstr) raise "Column not found for ordering: #{orderstr}." if !found orders << "#{table_def}`#{db.esc_col(orderstr)}`#{ordermode}" if orderstr end sql_order += orders.join(", ") list_args.delete("orderby") else raise "Unknown orderby object: #{list_args["orderby"].class.name}." end end list_args.each do |realkey, val| found = false if realkey.is_a?(Array) if !args[:joins_skip] datarow_obj = self.datarow_obj_from_args(args_def, list_args, realkey[0]) args = datarow_obj.columns_sqlhelper_args else args = args_def end do_joins[realkey[0].to_sym] = true table = "`#{db.esc_table(realkey[0])}`." key = realkey[1] else table = table_def args = args_def key = realkey end if args[:cols].key?(key) if val.is_a?(Array) escape_sql = Knj::ArrayExt.join( :arr => val, :callback => proc{|value| db.escape(value) }, :sep => ",", :surr => "'") sql_where += " AND #{table}`#{db.esc_col(key)}` IN (#{escape_sql})" elsif val.is_a?(Hash) and val[:type] == "col" if !val.key?(:table) Knj::Php.print_r(val) raise "No table was given for join." end do_joins[val[:table].to_sym] = true sql_where += " AND #{table}`#{db.esc_col(key)}` = `#{db.esc_table(val[:table])}`.`#{db.esc_col(val[:name])}`" elsif val.is_a?(Proc) call_args = Knj::Hash_methods.new(:ob => self, :db => db) sql_where += " AND #{table}`#{db.esc_col(key)}` = '#{db.esc(val.call(call_args))}'" else sql_where += " AND #{table}`#{db.esc_col(key)}` = '#{db.esc(val)}'" end found = true elsif args.key?(:cols_bools) and args[:cols_bools].index(key) != nil if val.is_a?(TrueClass) or (val.is_a?(Integer) and val.to_i == 1) or (val.is_a?(String) and (val == "true" or val == "1")) realval = "1" elsif val.is_a?(FalseClass) or (val.is_a?(Integer) and val.to_i == 0) or (val.is_a?(String) and (val == "false" or val == "0")) realval = "0" else raise "Could not make real value out of class: #{val.class.name} => #{val}." end sql_where += " AND #{table}`#{db.esc_col(key)}` = '#{db.esc(realval)}'" found = true elsif key.to_s == "limit_from" limit_from = val.to_i found = true elsif key.to_s == "limit_to" limit_to = val.to_i found = true elsif key.to_s == "limit" limit_from = 0 limit_to = val.to_i found = true elsif args.key?(:cols_dbrows) and args[:cols_dbrows].index("#{key.to_s}_id") != nil sql_where += " AND #{table}`#{db.esc_col(key.to_s + "_id")}` = '#{db.esc(val.id)}'" found = true elsif args.key?(:cols_str) and match = key.match(/^([A-z_\d]+)_(search|has)$/) and args[:cols_str].index(match[1]) != nil if match[2] == "search" Knj::Strings.searchstring(val).each do |str| sql_where += " AND #{table}`#{db.esc_col(match[1])}` LIKE '%#{db.esc(str)}%'" end elsif match[2] == "has" if val sql_where += " AND #{table}`#{db.esc_col(match[1])}` != ''" else sql_where += " AND #{table}`#{db.esc_col(match[1])}` = ''" end end found = true elsif match = key.match(/^([A-z_\d]+)_(not|lower)$/) and args[:cols].key?(match[1]) if match[2] == "not" sql_where += " AND #{table}`#{db.esc_col(match[1])}` != '#{db.esc(val)}'" elsif match[2] == "lower" sql_where += " AND LOWER(#{table}`#{db.esc_col(match[1])}`) = LOWER('#{db.esc(val)}')" else raise "Unknown mode: '#{match[2]}'." end found = true elsif args.key?(:cols_date) and match = key.match(/^(.+)_(day|month|from|to|below|above)$/) and args[:cols_date].index(match[1]) != nil val = Knj::Datet.in(val) if val.is_a?(Time) if match[2] == "day" sql_where += " AND DATE_FORMAT(#{table}`#{db.esc_col(match[1])}`, '%d %m %Y') = DATE_FORMAT('#{db.esc(val.dbstr)}', '%d %m %Y')" elsif match[2] == "month" sql_where += " AND DATE_FORMAT(#{table}`#{db.esc_col(match[1])}`, '%m %Y') = DATE_FORMAT('#{db.esc(val.dbstr)}', '%m %Y')" elsif match[2] == "from" or match[2] == "above" sql_where += " AND #{table}`#{db.esc_col(match[1])}` >= '#{db.esc(val.dbstr)}'" elsif match[2] == "to" or match[2] == "below" sql_where += " AND #{table}`#{db.esc_col(match[1])}` <= '#{db.esc(val.dbstr)}'" else raise "Unknown date-key: #{match[2]}." end found = true elsif args.key?(:cols_num) and match = key.match(/^(.+)_(from|to)$/) and args[:cols_num].index(match[1]) != nil if match[2] == "from" sql_where += " AND #{table}`#{db.esc_col(match[1])}` <= '#{db.esc(val)}'" elsif match[2] == "to" sql_where += " AND #{table}`#{db.esc_col(match[1])}` >= '#{db.esc(val)}'" else raise "Unknown method of treating cols-num-argument: #{match[2]}." end found = true elsif match = key.match(/^(.+)_lookup$/) and args[:cols].key?("#{match[1]}_id") and args[:cols].key?("#{match[1]}_class") sql_where += " AND #{table}`#{db.esc_col("#{match[1]}_class")}` = '#{db.esc(val.table)}'" sql_where += " AND #{table}`#{db.esc_col("#{match[1]}_id")}` = '#{db.esc(val.id)}'" found = true end list_args.delete(realkey) if found end args = args_def if !args[:joins_skip] raise "No joins defined on '#{args[:table]}' for: '#{args[:table]}'." if !do_joins.empty? and !args[:joined_tables] do_joins.each do |table_name, temp_val| raise "No join defined on table '#{args[:table]}' for table '#{table_name}'." if !args[:joined_tables].key?(table_name) table_data = args[:joined_tables][table_name] if table_data.key?(:parent_table) sql_joins += " LEFT JOIN `#{table_data[:parent_table]}` AS `#{table_name}` ON 1=1" else sql_joins += " LEFT JOIN `#{table_name}` ON 1=1" end if table_data[:ob] ob = table_data[:ob] else ob = self end class_name = args[:table].to_sym if table_data[:datarow] datarow = table_data[:datarow] else self.requireclass(class_name) if @objects.key?(class_name) datarow = @args[:module].const_get(class_name) end if !datarow.columns_sqlhelper_args ob.requireclass(datarow.table.to_sym) raise "No SQL-helper-args on class '#{datarow.table}' ???" if !datarow.columns_sqlhelper_args end newargs = datarow.columns_sqlhelper_args.clone newargs[:table] = table_name newargs[:joins_skip] = true ret = self.sqlhelper(table_data[:where].clone, newargs) sql_joins += ret[:sql_where] end end if limit_from and limit_to sql_limit = " LIMIT #{limit_from}, #{limit_to}" end return { :sql_joins => sql_joins, :sql_where => sql_where, :sql_limit => sql_limit, :sql_order => sql_order } end def datarow_obj_from_args(args, list_args, class_name) class_name = class_name.to_sym if !args.key?(:joined_tables) Knj::Php.print_r(list_args) Knj::Php.print_r(args) raise "No joined tables on '#{args[:table]}' to find datarow for: '#{class_name}'." end args[:joined_tables].each do |table_name, table_data| next if table_name.to_sym != class_name return table_data[:datarow] if table_data[:datarow] self.requireclass(class_name) if @objects.key?(class_name) return @args[:module].const_get(class_name) end raise "Could not figure out datarow for: '#{class_name}'." end end