team-umlaut/umlaut

View on GitHub
app/service_adaptors/service.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# Services are defined from the config/umlaut_config/services.yml file.
# hey should have the following properties
# id : unique internal string for the service, unique in yml file
# display_name : user displayable string
# url : A base url of some kind used by specific service
# type : Class name of class found in lib/service_adaptors to be used for logic
# priority: 0-9 (foreground) or a-z (background) for order of service operation
#
# Specific service_adaptor classes may have specific addtional configuration,
# commonly including 'password' or 'api_key'.
# specific service can put " required_config_parms :param1, :param2"
# in definition, for requirement exception raising on initialize.
#
# = Service Sub-classes
# Can include required config params in the class definition, eg:
#     required_config_params :api_key, :base_url
#
# Should define #service_types_generated returning an array of
# ServiceTypeValues.  This is neccesary for the Service to be
# run as a background service, and have the auto background updater
# work.
#
# The vast majority of services are 'standard' services, however
# there are other 'tasks' that a service can be. Well, right now, one
# other, 'link_out_filter'. The services 'task' config property
# sets the task/function/hook of the service. Default is 'standard'.
#
# A standard service defines handle(request)
#
# A link_out_filter service defines link_out_filter(request, url). If service
# returns a new url from filter_url, that's the url the user will be directed
# to. If service returns original url or nil, original url will still be used. 
#
# See documentation at ServiceResponse regarding how a service generates
# ServiceResponses to respond to a user request. 
class Service
  attr_reader :priority, :service_id, :url, :task, :status, :name, :group
  attr_accessor :request
  @@required_params_for_subclass = {} # initialize class var

  # Some constants for 'function' values
  StandardTask = 'standard'
  LinkOutFilterTask = 'link_out_filter'

  
  def initialize(config)
    config.each do | key, val |
      self.instance_variable_set(('@'+key).to_sym, val)
    end

    # task defaults to standard
    @task ||= StandardTask

    # check required params, and throw if neccesary

    required_params = Array.new
    # Some things required for all services
    required_params << "priority"
    # Custom things for this particular sub-class
    
    required_params.concat( @@required_params_for_subclass[self.class.name] ) if @@required_params_for_subclass[self.class.name]
    required_params.each do |param|
      begin
          value = self.instance_variable_get('@' + param.to_s)
          # docs say it raises a nameerror if it doesn't exist, docs
          # lie. So we'll just raise one ourselves, and catch it, to
          # handle both cases.
          raise NameError if value.nil?          
      rescue NameError
      raise ArgumentError.new("Missing Service configuration parameter. Service type #{self.class} (id: #{self.service_id}) requires a config parameter named '#{param}'. Check your config/umlaut_services.yml file.")
      end      
    end    
  end

  # Must be implemented by concrete sub-class. return an Array of 
  # ServiceTypeValues constituting the types of ServiceResponses the service
  # might generate. Used by Umlaut infrastructure including the background
  # service execution scheme itslef, as well asxml services returning 
  # information on services in progress.
  #
  # Example for a service that only generates fulltext: 
  #    return [ ServiceTypeValue[:fulltext] ]
  def service_types_generated
    raise Exception.new("#{self.class}: service_types_generated() must be implemented by Service concrete sub-class!")
  end


  # Method that should actually be called to trigger the service.
  # Will check pre-emption. 
  def handle_wrapper(request)
    unless ( preempted_by(request) )
      return handle(request)
    else
      # Pre-empted, log and close dispatch record as 'succesful'.
      Rails.logger.debug("Service #{service_id} was pre-empted and not run.")
      return request.dispatched(self, true)
    end
  end
  
  # Implemented by sub-class. Standard response-generating services implement
  # this method to do their work, generate responses and/or metadata. 
  def handle(request)
    raise Exception.new("#{self.class}: handle() must be implemented by Service concrete sub-class, for standard services!")
  end

  # This method is implemented by a concrete sub-class meant to
  # fulfill the task:link_out_filter. Will be called when the user clicks
  # on a url that will redirect external to Umlaut. The link_out_filter
  # service has the ability to intervene and record and/or change
  # the url. link_out_filters are called in order of priority config param
  # assigned, 0 through 9.
  #
  # orig_url is the current url umlaut is planning on sending the user to.
  # service_type is the ServiceType object responsible for this url.
  # the third argument is reserved for future use an options hash. 
  def link_out_filter(orig_url, service_response, other_args = {})
    raise Exception.new("#{self.class}: #link_out_filter must be implemented by Service concrete sub-class with task link_out_filter!")
  end

  # Name of this service, like "Amazon", or "OCLC Worldcat". 
  # First tries to look up an i18n translation using #translate, if not
  # found, uses a @display_name set in this service, if still not found
  # uses service_id for lack of anything else. 
  def display_name    
    self.translate("display_name", :default => @display_name || self.service_id)
  end



  # Sub-class can call class method like:
  #  required_config_params  :symbol1, :symbol2, symbol3
  # in class definition body. List of config parmas that
  # are required, exception will be thrown if not present. 
  def self.required_config_params(*params)
    params.each do |p|
      # Key on name of specific sub-class. Since this is a class
      # method, that should be self.name
      @@required_params_for_subclass[self.name] ||= Array.new
      a = @@required_params_for_subclass[self.name]
      a.push( p ) unless a.include?( p )
    end
  end




 # This method is called by Umlaut when user clicks on a service response. 
 # Default implementation here just returns response['url']. You can
 # over-ride in a sub-class to provide custom implementation of on-demand
 # url generation. Second argument is the http request params sent
 # by the client, used for service types that take form submissions (eg
 # search_inside). 
 # Should return a String url.
 def response_url(service_response, submitted_params )
   url = service_response[:url]
   raise "No url provided by service response" if url.nil? || url.empty?
   return url
 end

 # Look up an i18n key scoped to this service, first under the unique ID of
 # the service, then under the service class name:
 # * First look for translation under `umlaut.services.#{service_id.underscore}.key`
 # * If not found, look for translation under `umlaut.services.#{service_class_name.underscore}`
 # * If still not found, pass in optional default, otherwise you'll get I18n
 #    configured failure behavior. 
 #
 # second arg is options that can be passed to standard I18n.t, including defaults
 # and template arguments. 
 def translate(key, options = {})
    # Modify/add options[:default] to look up under class name too
    options[:default] = [:"umlaut.services.#{self.class.name.underscore}.#{key}"].concat(Array( options[:default] ))

    I18n.t("umlaut.services.#{self.service_id.underscore}.#{key}", options)
 end

 # Pre-emption hashes specify a combination of existing responses or
 # service executions that can pre-empt this service. Can specify
 # a service, a response type (ServiceTypeValue), or a combination of both.
 #
 # service's preempted_by property can either be a single pre-emption hash,
 # or an array of pre-emption hashes. 
 #
 # Can also specify that pre-emption is only of a certain service type
 # generated by self.
 #
 # The Service base class will enforce pre-emption and not even run
 # a service at all *so long as self_type is nil or '*' *. If the pre-emption
 # only applies to certain types generated by the service and not the entire
 # execution of the service, the concrete service subclass must implement
 # logic to do that. Calling the preempted method with the second argument
 # set will be helpful in writing this logic. 
 #
 # A preemption hash has string keys:
 #    existing_service: id of service that will pre-empt this service.
 #                      If key does not exist or is "*", then not specified,
 #                      any service. (existing_type will be specified). 
 #    existing_type:  ServiceTypeValue name that pre-empts this
 #                    service. "+" means that the service specified
 #                    in existing_service must have generated some
 #                    response, but type does not matter. "*" means
 #                    that the service specified in existing_service
 #                    must have completed succesfully, but may not
 #                    have generated any responses.
 #    self_type:      If blank or "*", preemption applies to any running
 #                    of this service at all. If set to a ServiceTypeValue
 #                    name, pre-emption is only of certain types generated
 #                    by this service. 
 def preempted_by(uml_request, for_type_generated=nil)
   preempted_by = @preempted_by
   return false if preempted_by.nil?
   preempted_by = [preempted_by] unless preempted_by.kind_of?(Array)
   preemption = nil

   preempted_by.each do | hash |
      service = hash["existing_service"] || "*"
      other_type = hash["existing_type"] || "*"      
      self_type = hash["self_type"] || "*"

      next unless (self_type == "*" || self_type == for_type_generated)

      if (other_type == "*")
        # Need to check dispatched services instead of service_types,
        # as we pre-empt even if no services created. 
        preemption = 
        uml_request.dispatched_services.to_a.find do |disp|
          service == "*" || 
          (disp.service_id == service &&
            (disp.status ==  DispatchedService::Successful ))
        end
      else
        # Check service responses
        preemption = Request.connection_pool.with_connection do 
            uml_request.service_responses.to_a.find do |response|
            ( other_type == "*" || other_type == "+" ||
              response.service_type_value.name == other_type)  &&
            ( service == "*" ||
              response.service_id == service)         
          end
        end
      end
      break if preemption
   end
   return (! preemption.nil? )
 end
 
 # used by render_service_credits helper method, returns
 # a hash with keys being a human-displayable name of a third party
 # to give 'credit' to, and value being a URL (or nil) to link the
 # name to. 
 # computed from @credits config variable, or returns empty hash. 
 def credits
   @credits || {}
 end

  
end