## RSence # Copyright 2006 Riassence Inc. # http://riassence.com/ # # You should have received a copy of the GNU General Public License along # with this software package. If not, contact licensing@riassence.com ## module RSence # HValue is the model for client-server synchronized values. # A value contains its payload {#data} and enough meta-data to define its behavior. class HValue # The validity of the data. Defaults to false, when the data comes from the client. # @return [Boolean] True, when set by the server. False when initially set by the client. Also false, unless all responders return true. attr_reader :is_valid # @private Is true when changed by the server. Causes the ValueManager to send its client-side representation. attr_reader :sync # @private The unique ID of the value. attr_reader :value_id alias val_id value_id # The payload data. Use {#set} to change. attr_reader :data # @private List of responders attr_reader :members # @private attr_writer :is_valid # @private def value_id=(new_id) @value_id = new_id end # @private alias val_id= value_id= alias valid is_valid alias valid? is_valid # @private Method for binding the value to the session data. def add( msg ) # get the value storage from the session data session_values = msg.session[:values][:by_id] ## Store the object here session_values[ @value_id ] = self ## Sends the client-side description restore( msg ) ## Set the valid flag, so we know that the value is initially in sync @is_valid = true end # @private (Re-)Send the client-size representation def restore( msg ) ## Tags itself as a new value from the client's point of view @is_new_to_client = true add_to_sync( msg ) end # Value meta-data. The third constructor parameter attr_accessor :meta # Creates a new client-server automatically synchronized data wrapper object. # @param [Message] msg Just pass on the +msg+ from the scope you call from. # @param [#to_json] data Any data that can be converted to JSON. # @param [Hash] meta Has no effect yet. def initialize( msg, data, meta = { :name => nil } ) ## Get an unique integer id for the value if RSence.args[:debug] and meta[:name] and not msg.valuemanager.id_exists?( msg, meta[:name] ) @value_id = meta[:name] else @value_id = msg.valuemanager.randgen.gen end @meta = meta ## set the data of the hvalue set( msg, data, true ) ## the @sync flag is raised, when the client data is older than the server data @sync = false ## the @is_valid flag is lowered, when the client data is newer than the server data @is_valid = true ## Bind the value to the value manager and report it to the client add( msg ) ## storage for validator bindings @members = {} end # Binds the value to a responder. The responder is an instance of {Plugins::Plugin__ Plugin} or {Plugins::GUIPlugin__ GUIPlugin}. # Responders are called once, when the client requests the data to be changed. # Responders must return +true+, if they accept the change. Otherwise the data is treated as invalid. # Responders must respond to exactly two parameters: ( (Message) +msg+, (HValue) +value+ ) # # @param [Symbol] plugin_name The name of the registered plugin to call with the +method_name+ # @param [Symbol] method_name The name of the method of the registered plugin +plugin_name+ to call. # @return [true] def bind( plugin_name, method_name ) plugin_name = plugin_name.to_sym unless plugin_name.class == Symbol method_name = method_name.to_sym unless method_name.class == Symbol @members[plugin_name] = [] unless @members.has_key?( plugin_name ) @members[plugin_name].push( method_name ) unless @members[plugin_name].include?( method_name ) return true end # Releases the responder of the value, both params as in bind, but optional +method_name+ can be omitted, matching all methods bound to the +plugin_name+. # @param [Symbol] plugin_name The name of the plugin acting as a responder to the value. # @param [Symbol] method_name The name of the method of the plugin acting as a responder to the value. # @return [Boolean] Returns true, if successful, false if not bound or other error. def release( plugin_name=false, method_name=false ) plugin_name = plugin_name.to_sym if plugin_name.class == String method_name = method_name.to_sym if method_name.class == String return release_all if not plugin_name and not method_name return false unless @members.has_key?( plugin_name ) if not method_name @members.delete( plugin_name ) else @members[plugin_name].slice!(@members[plugin_name].index( method_name )) if @members[plugin_name].include?(method_name) end return true end # Releases all responders. # @return [true] def release_all @members = {} return true end # @deprecated Use {#release} as the opposite to bind. alias unbind release # @private Tell all bound instances that the value is changed. def tell( msg ) invalid_count = 0 @members.each_key do |plugin_name| @members[plugin_name].each do |method_name| invalid_count += 1 unless msg.plugins.run_plugin( plugin_name, method_name, msg, self ) end end if invalid_count == 0 @is_valid = true msg.session[:values][:check].delete( @value_id ) end end # @private Handle updates from the client. def from_client( msg, data ) # only process changes, if different from the one already stored. if @data != data ## set takes care of the setting.. @data = data ## change the valid state, because the value was set by the client! @is_valid = false ## add the id to the values to be checked check_ids = msg.session[:values][:check] unless check_ids.include?( @value_id ) check_ids.push( @value_id ) end end end # @private Adds the value to the sync array. def add_to_sync( msg ) ## add the id to the values to be synchronized (to client) sync_ids = msg.session[:values][:sync] unless sync_ids.include?( @value_id ) sync_ids.push( @value_id ) end end # Sets the data of the value, the change will be synced with the client. # @param [Message] msg The {Message} instance. # @param [#to_json] data Any data that can be mapped to JSON and handled by the client. # @param [Boolean] dont_tell_client Doesn't notify the client about the change, if true. def set( msg, data, dont_tell_client=false ) @data = data # won't tell the client about the change, usually not needed unless dont_tell_client ## update the flags @sync = false @is_valid = true add_to_sync( msg ) end end # @private Tell the client that the value changed. def to_client( msg ) if @is_new_to_client ## Initialize a new client value init_str = "COMM.Values.create(#{@value_id.to_json},#{@data.to_json});" msg.reply_value( init_str ) @is_new_to_client = false else ## Sets the client value msg.reply_value "HVM.s(#{@value_id.to_json},#{@data.to_json});" end end # Destructor method. If msg is supplied, deletes the client representation too. # @param [false, Message] A {Message} instance. When supplied, deletes the client representation. def die!( msg=false ) release_all # get the value storage from the session data session_values = msg.session[:values][:by_id] ## Store the object here session_values.delete( @value_id ) if msg and not @is_new_to_client msg.reply_value("HVM.del(#{@value_id.to_json});") end end alias die die! end =begin class UploadValue < HValue @state_responders = { :ready => [], # id == 0 :started => [], # id == 1 :process => [], # id == 2 ; also uses bind 3 => [], # id == 3 4 => [], # id == 4 :error => [] # id < 0 } @upload_state = 0 @upload_key = '' @uploads = [] # the data should contain both state and key in the value def from_client( msg, data ) ## change the valid state, because the value was set by the client! @is_valid = data.include?(':::') # the state and key are separated by the ':::' delimiter string if @is_valid # split state and key using the delimiter (upload_state, upload_key) = data.split(':::') # the state is a number upload_state = upload_state.to_i @upload_state = upload_state @upload_key = upload_key # negative states are errors if upload_state < 0 # "upload error: #{upload_state}" # (parse the error) unless @state_responders[:error].empty? @state_responders[:error].each do |plugin_name,method_name| msg.run( plugin_name,method_name,msg,self,upload_state ) end end # the default state, 0 means the UI is ready to send an # upload and ticketserve is ready to receive it elsif upload_state == 0 # "upload state: ready to upload." # (do nothing) unless @state_responders[:ready].empty? @state_responders[:ready].each do |plugin_name,method_name| msg.run( plugin_name,method_name,msg,self,upload_state ) end end # this state means the upload's transfer is started and progressing elsif upload_state == 1 # "upload state: upload started." # (show progress bar) unless @state_responders[:started].empty? @state_responders[:started].each do |plugin_name,method_name| msg.run( plugin_name,method_name,msg,self,upload_state ) end end # this state means the upload's transfer is complete, # but the uploaded data hasn't been handled yet. elsif upload_state == 2 # "upload state: waiting to process." uploads = msg.plugins[:ticketservices].get_uploads(upload_key,true) if uploads.size == 1 uploaded_data = uploads[0] # only process changes, if different from the one already stored. if uploaded_data != @data @data = uploaded_data ## add the id to the values to be checked check_ids = msg.session[:values][:check] unless check_ids.include?( @value_id ) check_ids.push( @value_id ) end end msg.plugins[:ticketservices].del_uploads(upload_key,msg.ses_id) else # "upload, amount of uploads: #{uploads.size}" end # hvalue.set(msg,"3:::#{upload_key}") msg.console( "upload state: set to ack" ) elsif upload_state == 3 # "upload state: waiting for user ack." # (do nothing) msg.console( "upload state: waiting user ack" ) elsif upload_state == 4 # "upload state: user wants to upload again." # (set a new upload key, ) msg.console( "upload state: ack, getting new key" ) setup_upload( msg, hvalue ) else # "upload unknown state: #{upload_state.inspect}" end end return true end def setup_upload(msg,hvalue,size_bytes=500*1024,accept_mime=/image\/(.*?)/,allow_multi=false) upload_key = msg.plugins[:ticketservices].upload_key(msg,hvalue.val_id,size_bytes,accept_mime,allow_multi) hvalue.set( msg, upload_key ) end end =end end