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