less error info for the user
[opensuse:software-o-o.git] / app / models / seeker.rb
1 require 'md5'
2
3 class Seeker < ActiveXML::Base
4
5   def self.prepare_result(query, baseproject=nil, project=nil, exclude_filter=nil, exclude_debug=false)
6     cache_key = query
7     cache_key += "_#{baseproject}" if baseproject
8     cache_key += "_#{exclude_filter}" if exclude_filter
9     cache_key += "_#{exclude_debug}" if exclude_debug
10     cache_key += "_#{project}" if project
11     cache_key = 'searchresult_' + MD5::md5( cache_key ).to_s
12     Rails.cache.fetch(cache_key, :expires_in => 10.minutes) do
13       SearchResult.search(query, baseproject, project, exclude_filter, exclude_debug)
14     end
15   end
16
17
18   class SearchResult < Array
19     def self.search(query, baseproject, project=nil, exclude_filter=nil, exclude_debug=false)
20
21       query = query.gsub(/['"()]/, "")
22       words = query.split(" ").select {|part| !part.match(/^[0-9_\.-]+$/) }
23       versions = query.split(" ").select {|part| part.match(/^[0-9_\.-]+$/) }
24       logger.debug "splitted words and version: #{words.inspect} #{versions.inspect}"
25       raise "Please provide a valid search term" if words.blank?
26
27       xpath = "contains-ic(@name, " + words.select{|word| !word.match(/^%22.+%22$/) }.map{|word| "'#{word}'"}.join(", ") + ")"
28       words.select{|word| word.match(/^%22.+%22$/) }.map{|word| word.gsub( "%22", "" ) }.each do |word|
29         xpath = "@name = '#{word}' "
30       end
31       xpath += ' and ' + versions.map {|part| "starts-with(@version,'#{part}')"}.join(" and ") unless versions.blank?
32       xpath += " and path/project='#{baseproject}'" unless baseproject.blank?
33       xpath += " and @project = '#{project}' " unless project.blank?
34       xpath += " and not(contains-ic(@name, '-debuginfo')) and not(contains-ic(@name, '-debugsource'))" if exclude_debug
35       xpath += " and not(contains-ic(@project, '#{exclude_filter}'))" unless exclude_filter.blank?
36
37       bin = Seeker.find :binary, :match => xpath
38       pat = Seeker.find :pattern, :match => xpath
39       raise "Backend not responding" if( bin == nil && pat == nil )
40
41       result = new(query)
42       result.add_patlist(pat)
43       result.add_binlist(bin)
44
45       # remove this hack when the backend can filter for project names
46       result.reject!{|res| /#{exclude_filter}/.match( res.project ) } unless exclude_filter.blank?
47       result.sort! {|x,y| y.relevance <=> x.relevance}
48       return result
49     end
50
51     def self.cache
52       @cache ||= Cache.new
53     end
54
55     def inspect
56       "<Seeker::Searchresult ##{object_id} @length=#{size}>"
57     end
58
59     def self.logger
60       RAILS_DEFAULT_LOGGER
61     end
62
63     attr_reader :query
64     attr_reader :binary_count
65     attr_reader :pattern_count
66     attr_accessor :page_length
67
68     def initialize(query)
69       @query = query
70       @binary_count = 0
71       @pattern_count = 0
72       @page_length = 10
73       super()
74     end
75
76     # page index starts with 1
77     def page(idx)
78       return [] if idx > page_count
79       page = self[@page_length*(idx-1),@page_length]
80       page.each do |item|
81         item.update_description
82       end
83       return page
84     end
85
86     def page_count
87       #logger.debug "[SearchResult] calculating page_count: self.length: #{self.length}, @page_length: #@page_length"
88       ((self.length-1)/@page_length)+1
89     end
90
91     def add_binlist(binlist)
92       @index = Hash.new
93       @binary_count = 0
94       binlist.each_binary do |bin|
95         @binary_count += 1
96         fragment = Fragment.new(bin)
97         fragment.fragment_type = :binary
98         add_fragment(fragment)
99       end
100     end
101
102     def add_patlist(patlist)
103       @index = Hash.new
104       @pattern_count = 0
105       patlist.each_pattern do |pat|
106         @pattern_count += 1
107         fragment = Fragment.new(pat)
108         fragment.fragment_type = :pattern
109         add_fragment(fragment)
110       end
111     end
112
113     def add_fragment(fragment)
114       key = fragment.__key
115       if @index.has_key? key
116         item = @index[key]
117       else
118         case fragment.fragment_type
119         when :binary
120           item = Binary.new(key, @query)
121         when :pattern
122           item = Pattern.new(key, @query)
123         end
124         @index[key] = item
125         self << item
126       end
127       item.add_entry(fragment)
128       return item
129     end
130
131     def dump
132       out = "<ul>"
133       each do |item|
134         out << "<li>#{item.key} #{item.dump}</li>"
135       end
136       out << "</ul>"
137     end
138
139     class Item < Array
140       attr_accessor :query
141       attr_reader :key
142       attr_reader :name
143       attr_reader :project
144       attr_reader :repository
145       attr_reader :ymp_link
146       attr_reader :description
147       attr_reader :short_description
148       attr_reader :relevance
149
150       def initialize(key, query)
151         @key = key
152         @query = query
153         @relevance = 0
154       end
155
156       def cache
157         SearchResult.cache
158       end
159
160       def inspect
161         "<#{self.class.name} ##{object_id} @length=#{size}>"
162       end
163
164       def add_entry(element)
165         if element.__key != @key
166           raise "key mismatch: #{element.__key} != #@key"
167         end
168         self << element
169         cache_data(element) unless @data_cached
170         calculate_relevance unless @relevance_calculated
171       end
172
173       def dump
174         out = "<ul><li><b>Relevance:</b> #@relevance</li>"
175         each do |bin|
176           out << "<li>#{bin.filename} #{bin.dump}</li>"
177         end
178         out << "</ul>"
179         return out
180       end
181
182       def update_description
183         # implement in derived classes
184       end
185       
186       private
187
188       def cache_data(element)
189         @project = element.project
190         @repository = element.repository
191         @name = element.name
192         cache_specific_data(element)
193         @data_cached = true
194       end
195
196       def cache_specific_data(element)
197         # implement in derived classes
198       end
199
200       def calculate_relevance
201         quoted_query = Regexp.quote @query
202         @relevance_calculated = true
203         @relevance += 15 if name =~/^#{quoted_query}$/i
204         @relevance += 5 if name =~/^#{quoted_query}/i
205         @relevance += 15 if project =~ /^openSUSE:/i
206         @relevance += 5 if project =~ /^#{quoted_query}$/i
207         @relevance += 2 if project =~ /^#{quoted_query}/i
208         @relevance -= 5 if project =~ /unstable/i
209         @relevance -= 10 if project =~ /^home:/
210         calculate_specific_relevance
211       end
212
213       def calculate_specific_relevance
214         # implement in derived classes
215       end
216     end
217
218     class Binary < Item
219       attr_reader :arch_hash
220       attr_reader :filename
221       attr_reader :arch
222
223       def initialize(key, query)
224         super(key, query)
225         @arch_hash = Hash.new
226       end
227
228       def cache_specific_data(element)
229         @filename = element.filename
230         @arch = element.arch
231       end
232       
233       def calculate_specific_relevance
234         @relevance -= 10 if name =~ /-debugsource$/
235         @relevance -= 10 if name =~ /-debuginfo$/
236         @relevance -= 3 if name =~ /-devel$/
237         @relevance -= 3 if name =~ /-doc$/
238       end
239
240       def update_description
241         @description = cache.description(self)
242         return unless @description.nil?
243         return if self.empty?
244         begin
245           bin = self[0]
246           info = ::Published.find bin.filename, :view => "fileinfo", :project => @project,
247             :repository => @repository, :arch => bin.arch.to_s
248           if info.has_element? :description
249             @description = info.description.to_s
250           else
251             @description = ""
252           end
253         rescue ActiveXML::Transport::NotFoundError
254         rescue 
255           @description = ""
256         end
257         cache.store_description(self, @description)
258         return @description
259       end
260     end
261
262     class Pattern < Item
263       attr_reader :filename
264       attr_reader :filepath
265       attr_reader :repository
266       attr_reader :type
267
268       def calculate_specific_relevance
269         # pattern bonus
270         @relevance += 20
271       end
272       
273       def cache_specific_data(element)
274         @filename = element.filename.to_s
275         @filepath = element.filepath.to_s
276         @repository = element.repository.to_s
277         @type = element.type.to_s
278       end
279
280       def update_description
281         @description = cache.description(self)
282         return unless @description.nil?
283         begin
284           pat = ::Published.find @filename, :project => @project, :repository => @repository, :view => :fileinfo
285           if pat.has_element? :description
286             @description = pat.description.to_s
287           else
288             @description = ""
289           end
290         rescue
291           @description = ""
292         end
293         cache.store_description(self, @description)
294         return @description
295       end
296     end
297
298     class Fragment < Hash
299       attr_accessor :fragment_type
300
301       def initialize(element)
302         %w(project repository name filename filepath arch).each do |att|
303           self[att] = element.value att
304         end
305       end
306
307       def __key
308         @__key ||= @fragment_type.to_s+"|"+%w(project repository name).map{|x| self[x]}.join('|')
309       end
310
311       def dump
312         out = "<ul>"
313         each do |key,val|
314           out << "<li><b>#{key}:</b> #{val}</li>x"
315         end
316         out << "</ul>"
317         return out
318       end
319
320       def type(*args)
321         method_missing(:type,*args)
322       end
323
324       def method_missing(symbol,*args,&block)
325         if self.has_key? symbol.to_s
326           return self[symbol.to_s]
327         end
328         super(symbol,*args,&block)
329       end
330     end
331
332     class Cache
333       attr_accessor :active
334       def initialize
335         tmpdir = RAILS_ROOT+"/tmp/cache"
336         @desctmpdir = tmpdir+"/_descriptions"
337         @resulttmpdir = tmpdir+"/_searchresults"
338         @descexpire = 600.0
339         @resultexpire = 300.0
340         @active = true
341       end
342
343       def logger
344         RAILS_DEFAULT_LOGGER
345       end
346
347       def description(item)
348         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
349         return read(fname, @descexpire)
350       end
351
352       def searchresult(query, baseproject)
353         key = query + "|" + baseproject.to_s
354         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
355         if str = read(fname, @resultexpire)
356           return Marshal.load(str)
357         else
358           return nil
359         end
360       end
361
362       def store_description(item, desc)
363         fname = @desctmpdir + "/" + Digest::MD5.hexdigest(item.key)
364         write(fname, desc)
365       end
366
367       def store_searchresult(query, baseproject, result)
368         key = query + "|" + baseproject.to_s
369         fname = @resulttmpdir + "/" + Digest::MD5.hexdigest(key)
370         write(fname, Marshal.dump(result))
371       end
372
373       private
374       def read(fname, exp_time_sec)
375         return nil unless @active
376         begin
377           stat = File::stat(fname)
378         rescue Errno::ENOENT
379           return nil
380         end
381
382         if (Time.now-stat.mtime) < exp_time_sec
383           logger.debug "[Seeker::SearchResult::Cache] reading #{fname} from cache"
384           return File.read(fname)
385         else
386           return nil
387         end
388       end
389
390       def write(fname, data)
391         return unless @active
392         File.open(fname,"w") do |f|
393           logger.debug "[Seeker::SearchResult::Cache] writing #{fname} to cache"
394           f.write data
395         end
396       end
397     end
398   end
399 end