ignore nil param values
[opensuse:shared-resources.git] / buildservice / lib / activexml / transport.rb
1
2 module ActiveXML
3   module Transport
4
5     class Error < StandardError; end
6     class ConnectionError < Error; end
7     class UnauthorizedError < Error; end
8     class ForbiddenError < Error; end
9     class NotFoundError < Error; end
10     class NotImplementedError < Error; end
11
12     class Abstract
13       class << self
14         def register_protocol( proto )
15           ActiveXML::Config.register_transport self, proto.to_s
16         end
17
18         # spawn is called from within ActiveXML::Config::TransportMap.connect to
19         # generate the actual transport instance for a specific model. May be
20         # overridden in derived classes to implement some sort of connection
21         # cache or singleton transport objects. The default implementation is
22         # to create an own instance for each model.
23         def spawn( target_uri, opt={} )
24           self.new opt
25         end
26
27         def logger
28           ActiveXML::Base.config.logger
29         end
30       end
31
32       attr_accessor :target_uri
33
34       def initialize( target_uri, opt={} )
35       end
36
37       def find( model, *args )
38         raise NotImplementedError;
39       end
40
41       def query( model, query_string )
42         raise NotImplementedError;
43       end
44
45       def save( object )
46         raise NotImplementedError;
47       end
48
49       def login( user, password )
50         raise NotImplementedError;
51       end
52
53       def logger
54         ActiveXML::Base.config.logger
55       end
56     end
57
58
59
60
61     ##############################################
62     #
63     # BSSQL plugin
64     #
65     ##############################################
66
67
68
69     require 'builder'
70     class BSSQL < Abstract
71       register_protocol 'bssql'
72
73       class << self
74         def spawn( target_uri, opt={} )
75           @transport_obj ||= new( target_uri, opt )
76         end
77       end
78
79       def initialize( target_uri, opt={} )
80         logger.debug "[BSSQL] initialize( #{target_uri.inspect}, #{opt.inspect} )"
81
82         require 'db_project'
83         require 'db_package'
84
85         @xml_to_db_model_map = {
86           :project => "DbProject",
87           :package => "DbPackage"
88         }
89       end
90
91       def xml_to_db_model( xml_model )
92         unless @xml_to_db_model_map.has_key? xml_model
93           raise RuntimeError, "no model association defined for '#{xml_model.inspect}'"
94         end
95
96         case xml_model
97         when :project
98           return DbProject
99         when :package
100           return DbPackage
101         end
102       end
103
104       def find( model, *args )
105         logger.debug "[BSSQL] find( #{model.inspect}, #{args.inspect} )"
106
107         symbolified_model = model.name.downcase.to_sym
108         uri = ActiveXML::Config::TransportMap.target_for( symbolified_model )
109         options = ActiveXML::Config::TransportMap.options_for( symbolified_model )
110
111         # get matching database model class
112         db_model = xml_to_db_model( symbolified_model )
113
114         query = String.new
115         case args[0]
116         when String
117           params = args[1]
118         when Hash
119           params = args[0]
120         when Symbol
121           # :all
122           params = args[1]
123         else
124           raise Error, "illegal parameter to find"
125         end
126
127         query = query_from_options( params )
128         builder = Builder::XmlMarkup.new( :indent => 2 )
129
130         if( query.empty? )
131           items = db_model.find(:all) 
132         else
133           querymap = Hash.new
134           query.split( /\s+and\s+/ ).map {|x| x.split(/=/) }.each do |pair|
135             querymap[pair[0]] = pair[1]
136           end
137
138           join_fragments = Array.new
139           cond_fragments = Array.new
140           cond_values = Array.new
141
142           querymap.each do |k,v|
143             unless( md = k.match /^@(.*)/ )
144               raise NotFoundError, "Illegal query: [#{query}]"
145             end
146
147             v.gsub! /\*/, '%'
148
149             #unquote (I don't think this is safe enough...)
150             v.gsub! /^['"]/, ''
151             v.gsub! /['"]$/, ''
152
153             #FIXME: hack for project parameter in Package.find
154             if( symbolified_model == :package and md[1] == "project" )
155               join_fragments << "db_projects"
156
157               cond_fragments << ["db_packages.db_project_id = db_projects.id"]
158               cond_fragments << ["db_projects.name = ?"]
159
160               cond_values << v
161               next
162             end
163
164             unless( db_model.column_names.include? md[1] )
165               raise NotFoundError, "Unknown attribute '#{md[1]}' in query '#{query}'"
166             end
167
168             cond_fragments << ["#{db_model.table_name}.#{md[1]} LIKE BINARY ?"]
169             cond_values << v
170           end
171
172           joins = nil
173           unless join_fragments.empty?
174             joins = ", " + join_fragments.join(", ")
175             logger.debug "[BSSQL] join string: #{joins.inspect}"
176           end
177
178           conditions = [cond_fragments.join(" AND "), cond_values].flatten
179           logger.debug "[BSSQL] find conditions: #{conditions.inspect}"
180
181           items = db_model.find( :all, :select => "#{db_model.table_name}.*", :joins => joins, :conditions => conditions )
182         end
183         objects = Array.new
184         xml = String.new
185
186         if( args[0] == :all )
187           items.sort! {|a,b| a.name.downcase <=> b.name.downcase}
188           builder = Builder::XmlMarkup.new( :indent => 2 )
189           xml = builder.directory( :count => items.length ) do |dir|
190             items.each do |item|
191               dir.entry( :name => item.name )
192             end
193           end
194           return Directory.new( xml )
195         end
196
197         items.each do |item|
198           logger.debug "---> "+item.methods.grep(/^to_a/).inspect
199           #if not item.respond_to? :to_axml
200           #  raise RuntimeError, "unable to transform to xml: #{item.inspect}"
201           #end
202           obj = model.new( item.to_axml )
203           
204           obj.instance_variable_set( '@init_options', params )
205           objects << obj
206         end
207         
208         if objects.length > 1 || args[0] == :all
209           return objects
210         elsif objects.length == 1
211           return objects[0]
212         else
213           logger.debug "[BSSQL] query #{query} returned no objects"
214           raise NotFoundError, "#{model.name.downcase} query \"#{query}\" produced no results"
215         end
216       end
217
218       def login( user, password )
219         return true
220       end
221
222       def save( object )
223         logger.debug "[BSSQL] saving object #{object}"
224
225         db_model = xml_to_db_model(object.class.name.downcase.to_sym)
226
227         if db_model.respond_to? :store_axml
228           db_model.store_axml( object )
229         else
230           raise Error, "[BSSQL] Unable to store objects of type '#{object.class.name}'"
231         end
232       end
233
234       def query_from_options( opt_hash )
235         logger.debug "[BSSQL] query_from_options: #{opt_hash.inspect}"
236         query_fragments = Array.new
237         opt_hash.each do |k,v|
238           query_fragments << "@#{k}='#{v}'"
239         end
240         query = query_fragments.join( " and " )
241         logger.debug "[BSSQL] query_from_options: query is: '#{query}'"
242         return query
243       end
244
245       def xml_error( opt={} )
246         default_opts = {
247           :code => 500,
248           :summary => "Default summary",
249         }
250         opt = default_opts.merge opt
251
252         builder = Builder::XmlMarkup.new
253         xml = builder.status( :code => opt[:code] ) do |s|
254           s.summary( opt[:summary] )
255           s.details( opt[:details] ) if opt.has_key? :details
256         end
257
258         xml
259       end
260     end
261
262
263     ##############################################
264     #
265     # rest plugin
266     #
267     ##############################################
268
269
270     #TODO: put lots of stuff into base class
271
272     require 'base64'
273     require 'net/http'
274
275     class Rest < Abstract
276       register_protocol 'rest'
277       
278       class << self
279         def spawn( target_uri, opt={} )
280           @transport_obj ||= new( target_uri, opt )
281         end
282       end
283
284       def initialize( target_uri, opt={} )
285         logger.debug "[REST] initialize( #{target_uri.inspect}, #{opt.inspect} )"
286         @options = opt
287         if @options.has_key? :all
288           @options[:all].scheme = "http"
289         end
290         @http_header = {}
291       end
292
293       def target_uri=(uri)
294         uri.scheme = "http"
295         @target_uri = uri
296       end
297
298       def login( user, password )
299         @http_header ||= Hash.new
300         @http_header['Authorization'] = 'Basic ' + Base64.encode64( "#{user}:#{password}" )
301       end
302       
303       # returns document payload as string
304       def find( model, *args )
305         logger.debug "[REST] find( #{model.inspect}, #{args.inspect} )"
306         
307         params = Hash.new
308         symbolified_model = model.name.downcase.to_sym
309         uri = ActiveXML::Config::TransportMap.target_for( symbolified_model )
310         options = ActiveXML::Config::TransportMap.options_for( symbolified_model )
311         case args[0]
312         when Symbol
313           logger.debug "Transport.find: using symbol"
314           #raise "Illegal symbol, must be :all (or String/Hash)" unless args[0] == :all
315           uri = options[args[0]]
316           if args.length > 1
317             params = args[1].merge params
318           end
319         when String
320           logger.debug "Transport.find: using string"
321           params[:name] = args[0]
322           if args.length > 1
323             params = args[1].merge params
324           end
325         when Hash
326           logger.debug "Transport.find: using hash"
327           params = args[0]
328         else
329           raise "Illegal first parameter, must be Symbol/String/Hash"
330         end
331
332         logger.debug "uri is: #{uri}"
333         url = substitute_uri( uri, params )
334
335         obj = model.new( http_do( 'get', url ) )
336         obj.instance_variable_set( '@init_options', params )
337         return obj
338       end
339
340       def save( object )
341         logger.debug "saving #{object.inspect}"
342         url = substituted_uri_for( object )
343         http_do 'put', url, object.dump_xml
344       end
345
346
347       def delete( object, id )
348         # this is not very nice, but works(tm)
349         url = substituted_uri_for( object ) + "#{id}"
350         http_do 'delete', url
351       end
352
353
354       # defines an additional header that is passed to the REST server on every subsequent request
355       # e.g.: set_additional_header( "X-Username", "margarethe" )
356       def set_additional_header( key, value )
357         if value.nil? and @http_header.has_key? key
358           @http_header[key] = nil
359         end
360
361         @http_header[key] = value
362       end
363
364       # delete a header field set with set_additional_header
365       def delete_additional_header( key )
366         if @http_header.has_key? key
367           @http_header.delete key
368         end
369       end
370
371       def direct_http( url, opt={} )
372         defaults = {:method => "GET"}
373         opt = defaults.merge opt
374
375         #set default host if not set in uri
376         if not url.host
377           host, port = ActiveXML::Config::TransportMap.get_default_server( "rest" )
378           url.host = host
379           url.port = port unless port.nil?
380         end
381
382         logger.debug "--> direct_http url: #{url.inspect}"
383
384         http_do opt[:method], url, opt[:data]
385       end
386
387       #replaces the parameter parts in the uri from the config file with the correct values
388       def substitute_uri( uri, params )
389         u = uri.clone
390         u.scheme = "http"
391         u.path = URI.escape(uri.path.split(/\//).map { |x| x =~ /^:(\w+)/ ? params[$1.to_sym] : x }.join("/"))
392         if uri.query
393           new_pairs = []
394           pairs = u.query.split(/&/).map{|x| x.split(/=/, 2)}
395           pairs.each do |pair|
396             if pair.length == 2
397               pair[1] = params[pair[1].to_sym]
398               new_pairs << pair.join("=")
399             elsif pair.length == 1
400               pair[0] =~ /:(\w+)/
401               #new substitution rules:
402               #when param is not there, don't put anything in url
403               #when param is array, put multiple params in url
404               #any other case, stringify param and put it in url
405               next if not params.has_key? $1.to_sym or params[$1.to_sym].nil?
406               sub_val = params[$1.to_sym]
407               if sub_val.kind_of? Array
408                 sub_val.each do |val|
409                   new_pairs << $1 + "=" + val
410                 end
411               else
412                 new_pairs << $1 + "=" + sub_val.to_s
413               end
414             else
415               raise RuntimeError, "illegal url query pair: #{pair.inspect}"
416             end
417           end
418           u.query = URI.escape(new_pairs.join("&"))
419         end
420         u.path.gsub!(/\/+/, '/')
421         return u
422       end
423       private :substitute_uri
424
425       def substituted_uri_for( object )
426         symbolified_model = object.class.name.downcase.to_sym
427         uri = ActiveXML::Config::TransportMap.target_for( symbolified_model )
428         substitute_uri( uri, object.instance_variable_get("@init_options") )
429       end
430       private :substituted_uri_for
431
432       def http_do( method, url, data=nil )
433         logger.debug "http_do: url: #{url}"
434         begin
435           response = Net::HTTP.start(url.host, url.port) do |http|
436             path = url.path
437             if url.query
438               path += "?" + url.query
439             end
440             logger.debug "http_do: path: #{path}"
441
442             case method
443             when /get/i
444               http.get path, @http_header
445             when /put/i
446               raise "PUT without data" if data.nil?
447               http.put path, data, @http_header
448             when /post/i
449               raise "POST without data" if data.nil?
450               http.post path, data, @http_header
451             when /delete/i
452               http.delete path, @http_header
453             else
454               raise "unknown HTTP method: #{method.inspect}"
455             end
456           end
457         rescue SystemCallError => err
458           raise ConnectionError, "Failed to establish connection: "+err.message
459         end
460         handle_response( response )
461       end
462       private :http_do
463
464       def handle_response( http_response )
465         case http_response
466         when Net::HTTPSuccess, Net::HTTPRedirection
467           return http_response.read_body
468         when Net::HTTPNotFound
469           raise NotFoundError, http_response.read_body
470         when Net::HTTPUnauthorized
471           raise UnauthorizedError, http_response.read_body
472         when Net::HTTPForbidden
473           raise ForbiddenError, http_response.read_body
474         when Net::HTTPClientError, Net::HTTPServerError
475           raise Error, http_response.read_body
476         end
477         raise Error, http_response.read_body
478       end
479       private :handle_response
480       
481     end
482   end
483 end
484