Class: Request

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/request.rb

Overview

An ActiveRecord which represents a parsed OpenURL resolve service request, and other persistent state related to Umlaut’s handling of that OpenURL request) should not be confused with the Rails ActionController::Request class (which represents the complete details of the current ‘raw’ HTTP request, and is not stored persistently in the db).

Constituent openurl data is stored in Referent and Referrer.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.context_object_params(a_rails_request) ⇒ Object

input is a Rails request (representing http request) We pull out a hash of request params (get and post) that define a context object. We use CGI::parse instead of relying on Rails parsing because rails parsing ignores multiple params with same key value, which is legal in CGI.

So in general values of this hash will be an array. ContextObject.new_from_form_vars is good with that. Exception is url_ctx_fmt and url_ctx_val, which we’ll convert to single values, because ContextObject wants it so.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/request.rb', line 89

def self.context_object_params(a_rails_request)
  require 'cgi'
  
  # GET params
  co_params = CGI::parse( a_rails_request.query_string )    
  # add in the POST params please
  co_params.merge!(  CGI::parse(a_rails_request.raw_post)) if a_rails_request.raw_post
  # default value nil please, that's what ropenurl wants
  co_params.default = nil

  # CGI::parse annoyingly sometimes puts a nil key in there, for an empty
  # query param (like a url that has two consecutive && in it). Let's get rid
  # of it please, only confuses our code. 
  co_params.delete(nil)

  # Exclude params that are for Rails or Umlaut, and don't belong to the
  # context object. Except leave in umlaut.institution, that matters for
  # cachability. 
  excluded_keys = ["action", "controller", "page", /^umlaut\.(?!institution)/, 'rft.action', 'rft.controller']
  co_params.keys.each do |key|
    excluded_keys.each do |exclude|
      co_params.delete(key) if exclude === key;
    end
  end
  # 'id' is a special one, cause it can be a OpenURL 0.1 key, or
  # it can be just an application-level primary key. If it's only a
  # number, we assume the latter--an openurl identifier will never be
  # just a number.
  if co_params['id']
    co_params['id'].each do |id|       
      co_params['id'].delete(id) if id =~ /^\d+$/ 
    end
  end

  return co_params
end

.find_or_create(params, session, a_rails_request, options = {}) ⇒ Object

Either creates a new Request, or recovers an already created Request from the db–in either case return a Request matching the OpenURL. options => false, will not create a new request, return nil if no existing request can be found.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'app/models/request.rb', line 26

def self.find_or_create(params, session, a_rails_request, options = {} )
  # Pull out the http params that are for the context object,
  # returning a CGI::parse style hash, customized for what
  # ContextObject.new_from_form_vars wants. 
  co_params = self.context_object_params( a_rails_request )
  
  # Create a context object from our http params
  context_object = OpenURL::ContextObject.new_from_form_vars( co_params )
  # Sometimes umlaut puts in a 'umlaut.request_id' parameter.
  # first look by that, if we have it, for an existing request.  
  request_id = params['umlaut.request_id']

  # We're trying to identify an already existing response that matches
  # this request, in this session.  We don't actually match the
  # session_id in the cache lookup though, so background processing
  # will hook up with the right request even if user has no cookies. 
  # We don't check IP change anymore either, that was too open to
  # mistaken false negative when req.ip was being used. 
  req = Request.find_by_id(request_id) unless request_id.nil?
  
  # No match?  Just pretend we never had a request_id in url at all.
  request_id = nil if req == nil

  # Serialized fingerprint of openurl http params, suitable for looking
  # up in the db to see if we've seen it before. 
  param_fingerprint = self.co_params_fingerprint( co_params )
  client_ip = params['req.ip'] || a_rails_request.remote_ip()
  
  unless (req || params["umlaut.force_new_request"] == "true" || param_fingerprint.blank? )
    # If not found yet, then look for an existing request that had the same
    # openurl params as this one, in the same session. In which case, reuse.
    # Here we do require same session, since we don't have an explicit
    # request_id given.
    req = Request.where(
                :session_id => a_rails_request.session_options[:id],
                :contextobj_fingerprint => param_fingerprint, 
                :client_ip_addr => client_ip ).
        order("created_at DESC, id DESC").first
  end
  
  # Okay, if we found a req, it might NOT have a referent, it might
  # have been purged. If so, create a new one.
  if ( req && ! req.referent )
    req.referent = Referent.create_by_context_object(context_object)
  end

  unless (req || options[:allow_create] == false)
    # didn't find an existing one at all, just create one
    req = self.create_new_request!( :params => params, :session => session, :rails_request => a_rails_request, :contextobj_fingerprint => param_fingerprint, :context_object => context_object )
  end
  return req
end

Instance Method Details

#add_service_response(response_data) ⇒ Object

Create a ServiceResponse and it’s associated ServiceType(s) object, attached to this request. Arg is a hash of key/values. Keys MUST include:

  • :service, with the value being the actual Service object, not just the ID.

  • :service_type_value => the ServiceTypeValue object (or string name) for

the the ‘type’ of response this is.

Other keys are as conventional for the service. See documentation of conventional keys in ServiceResponse

Some keys end up stored in columns in the db directly, others end up serialized in a hash in a ‘text’ column, caller doesn’t have to worry about that, just pass em all in.

Eg, called from a service adapter plugin:

request.add_service_response(:service=>self, 
            :service_type_value => 'cover_image', 
            :display_text => 'Cover Image',  
            :url => img.inner_html, 
            :asin => asin, 
            :size => size)

Safe to call in thread, uses connection pool checkout.

Raises:

  • (ArgumentError)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/request.rb', line 200

def add_service_response(response_data)
  raise ArgumentError.new("missing required `:service` key") unless response_data[:service].kind_of?(Service)
  raise ArgumentError.new("missing required `:service_type_value` key") unless response_data[:service_type_value]
  
  svc_resp = nil
  ActiveRecord::Base.connection_pool.with_connection do
    svc_resp = ServiceResponse.new

    
    svc_resp.service_id = response_data[:service].service_id
    response_data.delete(:service)

    type_value =  response_data.delete(:service_type_value)
    type_value = ServiceTypeValue[type_value.to_s] unless type_value.kind_of?(ServiceTypeValue)      
    svc_resp.service_type_value = type_value

    svc_resp.request = self
    
    # response_data now includes actual key/values for the ServiceResponse
    # send em, take_key_values takes care of deciding which go directly
    # in columns, and which in serialized hash. 
    svc_resp.take_key_values( response_data )
          
    svc_resp.save!    
  end
    
  return svc_resp
end

#any_services_in_progress?Boolean

Returns:

  • (Boolean)


270
271
272
# File 'app/models/request.rb', line 270

def any_services_in_progress?
  return services_in_progress.length > 0
end

#can_dispatch?(service) ⇒ Boolean

Someone asks us if it’s okay to dispatch this guy. Only if it’s marked as Queued, or Failed—otherwise it should be already working, or done.

Returns:

  • (Boolean)


169
170
171
172
173
# File 'app/models/request.rb', line 169

def can_dispatch?(service)
  ds= self.dispatched_services.find(:first, :conditions=>{:service_id => service.service_id})
  
  return ds.nil? || (ds.status == DispatchedService::Queued) || (ds.status == DispatchedService::FailedTemporary)        
end

#dispatched(service, status, exception = nil) ⇒ Object

Method that registers the dispatch status of a given service participating in this request.

Status can be true (shorthand for DispatchedService::Success), false (shorthand for DispatchedService::FailedTemporary), or one of the other DispatchedService status codes. If a DispatchedService row already exists in the db, that row will be re-used, over-written with new status value.

Exception can optionally be provided, generally with failed statuses, to be stored for debugging purposes.

Safe to call in thread, uses explicit connectionpool checkout.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/models/request.rb', line 139

def dispatched(service, status, exception=nil)
  ActiveRecord::Base.connection_pool.with_connection do
    ds = self.find_dispatch_object( service )
    unless ds
      ds= self.new_dispatch_object!(service, status)
    end
    # In case it was already in the db, make sure to over-write status.
    # and add the exception either way.     
    ds.status = status
    ds.store_exception( exception )
    
    ds.save!
  end
end

#dispatched?(service) ⇒ Boolean

See can_dispatch below, you probably want that instead. This method checks to see if a particular service has been dispatched, and is succesful or in progress—that is, if this method returns false, you might want to dispatch the service (again). If it returns true though, don’t, it’s been done.

Returns:

  • (Boolean)


160
161
162
163
164
165
# File 'app/models/request.rb', line 160

def dispatched?(service)
  ds= self.dispatched_services.find(:first, :conditions=>{:service_id => service.service_id})
  # Return true if it exists and is any value but FailedTemporary.
  # FailedTemporary, it's worth running again, the others we shouldn't. 
  return (! ds.nil?) && (ds.status != DispatchedService::FailedTemporary)
end

#failed_service_dispatchesObject

Methods to look at status of dispatched services



231
232
233
234
235
# File 'app/models/request.rb', line 231

def failed_service_dispatches
  return self.dispatched_services.find(:all, 
    :conditions => ['status IN (?, ?)', 
    DispatchedService::FailedTemporary, DispatchedService::FailedFatal])
end

#get_service_type(svc_type, options = {}) ⇒ Object

pass in a ServiceTypeValue (or string name of such), get back list of ServiceResponse objects with that value belonging to this request. :refresh=>true will force a trip to the db to get latest values. otherwise, association is used.



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'app/models/request.rb', line 307

def get_service_type(svc_type, options = {})    
  svc_type_obj = (svc_type.kind_of?(ServiceTypeValue)) ? svc_type : ServiceTypeValue[svc_type]

  if ( options[:refresh])
    ActiveRecord::Base.connection_pool.with_connection do
      return self.service_responses.find(:all,
                              :conditions =>
                                ["service_type_value_name = ?",
                                svc_type_obj.name ]   
                              )
    end
  else
    # find on an assoc will go to db, unless we convert it to a plain
    # old array first.
    
    return self.service_responses.to_a.find_all { |response|
      response.service_type_value == svc_type_obj }      
  end
end

#new_dispatch_object!(service, status) ⇒ Object

Warning, doesn’t check for existing object first. Use carefully, usually paired with find_dispatch_object. Doesn’t actually call save though, caller must do that (in case caller wants to further initialize first).



331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'app/models/request.rb', line 331

def new_dispatch_object!(service, status)
  service_id = if service.kind_of?(Service)
    service.service_id
  else
    service.to_s
  end
  
  ds = DispatchedService.new
  ds.service_id = service_id
  ds.status = status
  self.dispatched_services << ds
  return ds
end

#service_type_in_progress?(svc_type) ⇒ Boolean

convenience method to call service_types_in_progress with one element.

Returns:

  • (Boolean)


253
254
255
# File 'app/models/request.rb', line 253

def service_type_in_progress?(svc_type)
  return service_types_in_progress?( [svc_type] )
end

#service_types_in_progress?(type_array) ⇒ Boolean

pass in array of ServiceTypeValue or string name of same. Returns true if ANY of them are in progress.

Returns:

  • (Boolean)


259
260
261
262
263
264
265
266
267
268
# File 'app/models/request.rb', line 259

def service_types_in_progress?(type_array)
  # convert strings to ServiceTypeValues
  type_array = type_array.collect {|s|  s.kind_of?(ServiceTypeValue)? s : ServiceTypeValue[s] }
  
  self.services_in_progress.each do |s|
    # array intersection
    return true unless (s.service_types_generated & type_array).empty? 
  end
  return false;
end

#services_in_progressObject

Returns array of Services in progress or queued. Intentionally uses cached in memory association, so it wont’ be a trip to the db every time you call this.



240
241
242
243
244
245
246
247
248
249
250
251
# File 'app/models/request.rb', line 240

def services_in_progress
  # Intentionally using the in-memory array instead of going to db.
  # that's what the "to_a" is. Minimize race-condition on progress
  # check, to some extent, although it doesn't really get rid of it.
  dispatches = self.dispatched_services.to_a.find_all do | ds |
    (ds.status == DispatchedService::Queued) || 
    (ds.status == DispatchedService::InProgress)
  end

  svcs = dispatches.collect { |ds| ds.service }
  return svcs
end

#title_level_citation?Boolean

Is the citation represetned by this request a title-level only citation, with no more specific article info? Or no, does it include article or vol/iss info?

Returns:

  • (Boolean)


289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'app/models/request.rb', line 289

def title_level_citation?
  data = referent.

  # atitle can't generlaly get us article-level, but it can with
  # lexis nexis, so we'll consider it article-level. Since it is!
  return ( data['atitle'].blank? &&
           data['volume'].blank? &&
           data['issue'].blank? &&            
      # pmid or doi is considered article-level, because SFX can
      # respond to those. Other identifiers may be useless. 
      (! referent.identifiers.find {|i| i =~ /^info\:(doi|pmid)/})
      )
end

#to_context_objectObject



274
275
276
277
278
279
280
281
282
283
284
# File 'app/models/request.rb', line 274

def to_context_object
  #Mostly just the referent
  context_object = self.referent.to_context_object

  #But a few more things
  context_object.referrer.add_identifier(self.referrer_id) if self.referrer_id

  context_object.requestor.('ip', self.client_ip_addr) if self.client_ip_addr

  return context_object
end