class Knj::Objects attr_reader :args, :events 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] @objects = {} @objects_mutex = Mutex.new @events = Knj::Event_handler.new @events.add_event( :name => :no_html, :connections_max => 1 ) raise "No DB given." if !@args[:db] raise "No class path given." if !@args[:class_path] and (@args[:require] or !@args.has_key?(:require)) 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_mutex.synchronize do @objects.each do |classn, newhash| objs_cloned[classn] = newhash.clone end end return objs_cloned end def db return @args[:db] end def count_objects count = 0 @objects_mutex.synchronize do @objects.each do |key, value| value.each do |id, object| count += 1 end end end return count end def connect(args, &block) raise "No object given." if !args["object"] raise "No signals given." if !args.has_key?("signal") and !args.has_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[classstr] @callbacks[classstr].clone.each do |callback_key, callback| docall = false if callback.has_key?("signal") and args.has_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) return nil if !@args[:require] and @args.has_key?(:require) classname = classname.to_s if !Knj::Php.class_exists(classname) filename = @args[:class_path] + "/#{@args[:class_pre]}#{classname.downcase}.rb" filename_req = @args[:class_path] + "/#{@args[:class_pre]}#{classname.downcase}" raise "Class file could not be found: #{filename}." if !File.exists?(filename) require filename_req end end def get(classname, data) classname = classname.to_sym if 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 data.is_a?(Integer) or data.is_a?(String) or data.is_a?(Fixnum) id = data.to_i elsif raise Knj::Errors::InvalidData, "Unknown data: '#{data.class.to_s}'." end return @objects[classname][id] if @objects.has_key?(classname) and @objects[classname].has_key?(id) retobj = nil @objects_mutex.synchronize do if !@objects.has_key?(classname) self.requireclass(classname) @objects[classname] = {} end if @args[:datarow] @objects[classname][id] = @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] @objects[classname][id] = @args[:module].const_get(classname).new(*args) end return @objects[classname][id] end raise "Something went wrong." 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 Knj::Php.class_exists("Dictionary") list = Dictionary.new else list = Hash.new 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.has_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.has_key?(classname) #raise "Could not find object class in cache: #{classname}." #elsif !@objects[classname].has_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_mutex.synchronize do @objects[classname].delete(object.id.to_i) end #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.has_key?(classname) @objects_mutex.synchronize do @objects[classname] = {} end 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) if classn.is_a?(Array) classn.each do |realclassn| self.clean(realclassn) end else return false if !@objects.has_key?(classn) @objects_mutex.synchronize do @objects[classn] = {} GC.start end end end def clean_all classnames = [] @objects_mutex.synchronize do @objects.each do |classn, hash_list| classnames << classn end classnames.each do |classn| @objects[classn] = {} end end GC.start end def clean_recover @objects_mutex.synchronize do @objects.each do |classn, hash_list| classobj = Kernel.const_get(classn) ObjectSpace.each_object(classobj) do |obj| @objects[classn][obj.id] = obj end end end end def sqlhelper(list_args, args) if args[:db] db = args[:db] else db = @args[:db] end if args[:table] table = "`#{db.esc_table(args[:table])}`." else table = "" end sql_where = "" sql_order = "" sql_limit = "" limit_from = nil limit_to = nil cols_str_has = args.has_key?(:cols_str) cols_num_has = args.has_key?(:cols_num) cols_date_has = args.has_key?(:cols_date) cols_dbrows_has = args.has_key?(:cols_dbrows) cols_bools_has = args.has_key?(:cols_bools) if list_args.has_key?("orderby") orders = [] orderstr = list_args["orderby"] if list_args["orderby"].is_a?(String) found = false found = true if !found and cols_str_has and args[:cols_str].index(orderstr) != nil found = true if !found and cols_date_has and args[:cols_date].index(orderstr) != nil found = true if !found and cols_num_has and args[:cols_num].index(orderstr) != nil if found sql_order += " ORDER BY " ordermode = " ASC" if list_args.has_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}`#{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| 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" else raise "Unknown object: #{val.class.name}" end found = false found = true if !found and cols_str_has and args[:cols_str].index(orderstr) != nil found = true if !found and cols_date_has and args[:cols_date].index(orderstr) != nil found = true if !found and cols_num_has and args[:cols_num].index(orderstr) != nil found = true if !found and cols_bools_has and args[:cols_bools].index(orderstr) != nil raise "Column not found for ordering: #{orderstr}." if !found orders << "#{table}`#{db.esc_col(orderstr)}`#{ordermode}" 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 |key, val| found = false if (cols_str_has and args[:cols_str].index(key) != nil) or (cols_num_has and args[:cols_num].index(key) != nil) or (cols_dbrows_has and args[:cols_dbrows].index(key) != nil) 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})" else sql_where += " AND #{table}`#{db.esc_col(key)}` = '#{db.esc(val)}'" end found = true elsif cols_bools_has 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 cols_dbrows_has 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 cols_str_has 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$/) and ((cols_str_has and args[:cols_str].index(match[1]) != nil) or (cols_num_has and args[:cols_num].index(match[1]) != nil)) sql_where += " AND #{table}`#{db.esc_col(match[1])}` != '#{db.esc(val)}'" found = true elsif cols_date_has and match = key.match(/^(.+)_(day|month|from|to)$/) and args[:cols_date].index(match[1]) != nil 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" sql_where += " AND #{table}`#{db.esc_col(match[1])}` >= '#{db.esc(val.dbstr)}'" elsif match[2] == "to" sql_where += " AND #{table}`#{db.esc_col(match[1])}` <= '#{db.esc(val.dbstr)}'" else raise "Unknown date-key: #{match[2]}." end found = true elsif cols_num_has 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 end list_args.delete(key) if found end if limit_from and limit_to sql_limit = " LIMIT #{limit_from}, #{limit_to}" end return { :sql_where => sql_where, :sql_limit => sql_limit, :sql_order => sql_order } end end