dm-keeper-adapter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/README.rdoc +40 -0
- data/Rakefile +29 -0
- data/dm-keeper-adapter.gemspec +27 -0
- data/lib/dm-keeper-adapter.rb +78 -0
- data/lib/dm-keeper-adapter/create.rb +21 -0
- data/lib/dm-keeper-adapter/delete.rb +12 -0
- data/lib/dm-keeper-adapter/misc.rb +9 -0
- data/lib/dm-keeper-adapter/read.rb +168 -0
- data/lib/dm-keeper-adapter/update.rb +12 -0
- data/lib/dm-keeper-adapter/version.rb +8 -0
- data/lib/keeper/container.rb +58 -0
- data/lib/keeper/feature.rb +31 -0
- data/lib/keeper/product.rb +14 -0
- data/lib/keeper/productline.rb +7 -0
- data/lib/keeper/relation.rb +13 -0
- data/lib/keeper/relationtree.rb +20 -0
- data/lib/keeper/valuelist.rb +99 -0
- data/lib/property/xml.rb +18 -0
- data/script/README.rdoc +12 -0
- data/script/generate_class.rb +10 -0
- data/script/genfeatureclass.rb +52 -0
- data/script/show-containers.rb +22 -0
- data/test/helper.rb +3 -0
- data/test/test_get_feature.rb +19 -0
- data/test/test_get_relationtree.rb +19 -0
- metadata +125 -0
data/.gitignore
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= dm-keeper-adapter
|
2
|
+
|
3
|
+
A DataMapper adapter for features.opensuse.org
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
This adapter allows easy access to the openSUSE feature database using
|
8
|
+
DataMapper. It supports features, relationtrees, productlines, and
|
9
|
+
products.
|
10
|
+
|
11
|
+
features.opensuse.org is the web frontend, the database server
|
12
|
+
provides an xquery API at https://keeper.novell.com/sxkeeper, which is
|
13
|
+
used by this adapter - hence the name.
|
14
|
+
|
15
|
+
== Install
|
16
|
+
|
17
|
+
Install it with
|
18
|
+
|
19
|
+
gem install dm-keeper-adapter
|
20
|
+
|
21
|
+
== Code
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'dm-core'
|
25
|
+
require 'dm-keeper-adapter'
|
26
|
+
|
27
|
+
DataMapper::Logger.new($stdout, :debug)
|
28
|
+
|
29
|
+
# Retrieves credentials from ~/.oscrc if exists
|
30
|
+
# Otherwise add user:pass@ before keeper.novell.com
|
31
|
+
keeper = DataMapper.setup(:default, :adapter => 'keeper', :url => 'https://keeper.novell.com/sxkeeper')
|
32
|
+
|
33
|
+
require 'keeper/feature'
|
34
|
+
DataMapper.finalize
|
35
|
+
|
36
|
+
f = Feature.get(311545)
|
37
|
+
|
38
|
+
== TODO
|
39
|
+
|
40
|
+
Real DataMapper queries mapped to XPath
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
extra_docs = ['README*', 'TODO*', 'CHANGELOG*']
|
5
|
+
|
6
|
+
task :default => [:test]
|
7
|
+
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.libs << File.expand_path('../test', __FILE__)
|
10
|
+
t.libs << File.expand_path('../', __FILE__)
|
11
|
+
t.test_files = FileList['test/test*.rb']
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'yard'
|
17
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
18
|
+
t.files = ['lib/**/*.rb', *extra_docs]
|
19
|
+
t.options = ['--no-private']
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
STDERR.puts "Install yard if you want prettier docs"
|
23
|
+
require 'rdoc/task'
|
24
|
+
Rake::RDocTask.new(:doc) do |rdoc|
|
25
|
+
rdoc.rdoc_dir = "doc"
|
26
|
+
rdoc.title = "dm-keeper-adapter #{DataMapper::Adapters::KeeperAdapter::VERSION}"
|
27
|
+
extra_docs.each { |ex| rdoc.rdoc_files.include ex }
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dm-keeper-adapter/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dm-keeper-adapter"
|
7
|
+
s.version = DataMapper::Adapters::KeeperAdapter::VERSION
|
8
|
+
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Klaus Kämpf"]
|
11
|
+
s.email = ["kkaempf@suse.de"]
|
12
|
+
s.homepage = ""
|
13
|
+
s.summary = %q{A datamapper adapter for FATE (aka keeper.novell.com)}
|
14
|
+
s.description = %q{Use it in Ruby applications to access FATE}
|
15
|
+
|
16
|
+
# get credentials from ~/.oscrc
|
17
|
+
s.add_dependency("inifile", ["~> 0.4.1"])
|
18
|
+
# parse xml response
|
19
|
+
s.add_dependency("nokogiri", ["~> 1.4"])
|
20
|
+
|
21
|
+
s.rubyforge_project = "dm-keeper-adapter"
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'dm-core/adapters/abstract_adapter'
|
4
|
+
|
5
|
+
require "dm-keeper-adapter/create"
|
6
|
+
require "dm-keeper-adapter/read"
|
7
|
+
require "dm-keeper-adapter/update"
|
8
|
+
require "dm-keeper-adapter/delete"
|
9
|
+
require "dm-keeper-adapter/misc"
|
10
|
+
|
11
|
+
|
12
|
+
module DataMapper
|
13
|
+
class Property
|
14
|
+
autoload :XML, "property/xml"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
module DataMapper::Adapters
|
20
|
+
|
21
|
+
class KeeperAdapter < AbstractAdapter
|
22
|
+
OSCRC_CREDENTIALS = "https://api.opensuse.org"
|
23
|
+
|
24
|
+
def initialize(name, options)
|
25
|
+
super
|
26
|
+
require 'net/https'
|
27
|
+
require 'uri'
|
28
|
+
@uri = URI.parse(options[:url])
|
29
|
+
@username = options[:username]
|
30
|
+
@password = options[:password]
|
31
|
+
unless @username && @password
|
32
|
+
require 'inifile'
|
33
|
+
oscrc = IniFile.new(File.join(ENV['HOME'], '.oscrc'))
|
34
|
+
if oscrc.has_section?(OSCRC_CREDENTIALS)
|
35
|
+
@username = oscrc[OSCRC_CREDENTIALS]['user']
|
36
|
+
@password = oscrc[OSCRC_CREDENTIALS]['pass']
|
37
|
+
raise "No .oscrc credentials for keeper.novell.com" unless @username && @password
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@connection = Net::HTTP.new( @uri.host, @uri.port )
|
41
|
+
# from http://www.rubyinside.com/nethttp-cheat-sheet-2940.html
|
42
|
+
@connection.use_ssl = true if @uri.scheme == "https"
|
43
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def get(path)
|
48
|
+
# STDERR.puts "Get #{@uri.path} + #{path}"
|
49
|
+
request = Net::HTTP::Get.new @uri.path + path
|
50
|
+
request.basic_auth @username, @password
|
51
|
+
response = @connection.request request
|
52
|
+
raise ArgumentError unless response
|
53
|
+
raise( RuntimeError, "Server returned #{response.code}" ) unless response.code.to_i == 200
|
54
|
+
# $stderr.puts "#{response.inspect}"
|
55
|
+
# response.each { |k,v| $stderr.puts "#{k}: #{v}" }
|
56
|
+
# $stderr.puts "Encoding >#{response['content-encoding']}<"
|
57
|
+
# check
|
58
|
+
# content-type: text/xml;charset=UTF-8
|
59
|
+
# content-encoding: gzip
|
60
|
+
body = response.body
|
61
|
+
if response['content-encoding'] == "gzip"
|
62
|
+
require 'zlib'
|
63
|
+
require 'stringio'
|
64
|
+
# http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-string-in-ruby
|
65
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
66
|
+
end
|
67
|
+
case response['content-type']
|
68
|
+
when /xml/
|
69
|
+
Nokogiri::XML body
|
70
|
+
when /html/
|
71
|
+
Nokogiri::HTML body
|
72
|
+
else
|
73
|
+
raise TypeError, "Unknown content-type #{response['content-type']}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# create.rb
|
2
|
+
module DataMapper::Adapters
|
3
|
+
class KeeperAdapter < AbstractAdapter
|
4
|
+
def create(resources)
|
5
|
+
table_groups = group_resources_by_table(resources)
|
6
|
+
table_groups.each do |table, resources|
|
7
|
+
# make
|
8
|
+
# class User
|
9
|
+
# property :id, Serial
|
10
|
+
# end
|
11
|
+
# work
|
12
|
+
resources.each do |resource|
|
13
|
+
initialize_serial(resource,
|
14
|
+
worksheet_record_cound(table)+1)
|
15
|
+
post_resource_to_worksheet(resource,table)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
resources.size
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# delete.rb
|
2
|
+
module DataMapper::Adapters
|
3
|
+
class KeeperAdapter < AbstractAdapter
|
4
|
+
def delete(collection)
|
5
|
+
each_resource_with_edit_url(collection) do |resource, edit_url|
|
6
|
+
connection.delete(edit_url, 'If-Match' => "*")
|
7
|
+
end
|
8
|
+
# return count
|
9
|
+
collection.size
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# read.rb
|
2
|
+
module DataMapper::Adapters
|
3
|
+
class KeeperAdapter < AbstractAdapter
|
4
|
+
require 'nokogiri'
|
5
|
+
def read(query)
|
6
|
+
records = records_for(query)
|
7
|
+
# query.filter_records(records)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
# taken from https://github.com/whoahbot/dm-redis-adapter/
|
12
|
+
|
13
|
+
##
|
14
|
+
# Retrieves records for a particular model.
|
15
|
+
#
|
16
|
+
# @param [DataMapper::Query] query
|
17
|
+
# The query used to locate the resources
|
18
|
+
#
|
19
|
+
# @return [Array]
|
20
|
+
# An array of hashes of all of the records for a particular model
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def records_for(query)
|
24
|
+
# $stderr.puts "records_for(#{query})"
|
25
|
+
# $stderr.puts "records_for(#{query.inspect})"
|
26
|
+
records = []
|
27
|
+
if query.conditions.nil?
|
28
|
+
# return all
|
29
|
+
else
|
30
|
+
query.conditions.operands.each do |operand|
|
31
|
+
if operand.is_a?(DataMapper::Query::Conditions::OrOperation)
|
32
|
+
operand.each do |op|
|
33
|
+
records = records + perform_query(query, op)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
records = perform_query(query, operand)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
records
|
41
|
+
end #def
|
42
|
+
|
43
|
+
##
|
44
|
+
# Find records that match have a matching value
|
45
|
+
#
|
46
|
+
# @param [DataMapper::Query] query
|
47
|
+
# The query used to locate the resources
|
48
|
+
#
|
49
|
+
# @param [DataMapper::Operation] the operation for the query
|
50
|
+
#
|
51
|
+
# @return [Array]
|
52
|
+
# An array of hashes of all of the records for a particular model
|
53
|
+
#
|
54
|
+
# @api private
|
55
|
+
def perform_query(query, operand)
|
56
|
+
# $stderr.puts "perform_query(#{query},#{operand})"
|
57
|
+
records = []
|
58
|
+
|
59
|
+
if operand.is_a?(DataMapper::Query::Conditions::NotOperation)
|
60
|
+
subject = operand.first.subject
|
61
|
+
value = operand.first.value
|
62
|
+
elsif operand.subject.is_a?(DataMapper::Associations::ManyToOne::Relationship)
|
63
|
+
subject = operand.subject.child_key.first
|
64
|
+
value = operand.value[operand.subject.parent_key.first.name]
|
65
|
+
else
|
66
|
+
subject = operand.subject
|
67
|
+
value = operand.value
|
68
|
+
end
|
69
|
+
|
70
|
+
if subject.is_a?(DataMapper::Associations::ManyToOne::Relationship)
|
71
|
+
subject = subject.child_key.first
|
72
|
+
end
|
73
|
+
|
74
|
+
# typical queries
|
75
|
+
#
|
76
|
+
# ?query=/feature[
|
77
|
+
# productcontext[
|
78
|
+
# not (status[done or rejected or duplicate or unconfirmed])
|
79
|
+
# ]
|
80
|
+
# and
|
81
|
+
# actor[
|
82
|
+
# (person/userid='kkaempf@novell.com' or person/email='kkaempf@novell.com' or person/fullname='kkaempf@novell.com')
|
83
|
+
# and
|
84
|
+
# role='projectmanager'
|
85
|
+
# ]
|
86
|
+
# ]
|
87
|
+
#
|
88
|
+
# "/#{container}[actor/role='infoprovider']
|
89
|
+
#
|
90
|
+
# query=/feature[title='Foo%20bar%20baz']
|
91
|
+
#
|
92
|
+
# query=/feature[contains(title,'Foo')]
|
93
|
+
# query=/feature[contains(title,'Foo')]/title
|
94
|
+
# query=/feature[contains(title,'Foo')]/@k:id
|
95
|
+
#
|
96
|
+
|
97
|
+
# $stderr.puts "perform_query(subject #{subject.inspect},#{value.inspect})"
|
98
|
+
container = query.model.to_s.downcase
|
99
|
+
if query.model.key.include?(subject)
|
100
|
+
# get single <feature>
|
101
|
+
records << node_to_record(query.model, get("/#{container}/#{value}").root)
|
102
|
+
else
|
103
|
+
# query, get <collection>[<object><feature>...]*
|
104
|
+
xpath = "/#{container}["
|
105
|
+
# ...
|
106
|
+
xpath << "]"
|
107
|
+
collection = get("/#{container}?query=#{xpath}")
|
108
|
+
collection.xpath("/#{container}").each do |feature|
|
109
|
+
records << node_to_record(query.model, feature)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
records
|
114
|
+
end # def
|
115
|
+
|
116
|
+
##
|
117
|
+
# Convert feature (as xml) into record (as hash of key/value pairs)
|
118
|
+
#
|
119
|
+
# @param [Nokogiri::XML::Node] node
|
120
|
+
# A node
|
121
|
+
#
|
122
|
+
# @return [Hash]
|
123
|
+
# A hash of all of the properties for a particular record
|
124
|
+
#
|
125
|
+
# @api private
|
126
|
+
def node_to_record(model, node)
|
127
|
+
record = { }
|
128
|
+
xpathmap = model.xpathmap rescue { }
|
129
|
+
xmlnamespaces = model.xmlnamespaces rescue nil
|
130
|
+
# $stderr.puts "node_to_record(#{model}:#{node.class})"
|
131
|
+
model.properties.each do |property|
|
132
|
+
xpath = xpathmap[property.name] || property.name
|
133
|
+
key = property.name.to_s
|
134
|
+
children = node.xpath("./#{xpath}", xmlnamespaces)
|
135
|
+
# $stderr.puts "Property found: #{property.inspect}"
|
136
|
+
case children.size
|
137
|
+
when 0: next
|
138
|
+
when 1: value = children.text
|
139
|
+
else
|
140
|
+
value = children.to_xml
|
141
|
+
end
|
142
|
+
# $stderr.puts "Key #{key}, Value #{value} <#{property.class}>"
|
143
|
+
case property
|
144
|
+
when DataMapper::Property::Date
|
145
|
+
require 'parsedate'
|
146
|
+
record[key] = Time.utc(ParseDate.parsedate(value))
|
147
|
+
when DataMapper::Property::Integer
|
148
|
+
record[key] = value.to_s
|
149
|
+
when DataMapper::Property::String
|
150
|
+
record[key] = value.to_s
|
151
|
+
else
|
152
|
+
raise TypeError, "#{property} unsupported"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
model.relationships.each do |rel|
|
156
|
+
# $stderr.puts "Rel ? #{rel.inspect}"
|
157
|
+
children = node.xpath("./#{rel.child_model_name.downcase}")
|
158
|
+
value = []
|
159
|
+
while n = children.shift
|
160
|
+
value << node_to_record(rel.child_model, n)
|
161
|
+
end
|
162
|
+
record[rel.name.to_s] = value
|
163
|
+
# $stderr.puts "#{rel.name} -> #{value.inspect}"
|
164
|
+
end
|
165
|
+
record
|
166
|
+
end
|
167
|
+
end # class
|
168
|
+
end # module
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# update.rb
|
2
|
+
module DataMapper::Adapters
|
3
|
+
class KeeperAdapter < AbstractAdapter
|
4
|
+
def update(attributes, collection)
|
5
|
+
each_resource_with_edit_url(collection) do |resource, edit_url|
|
6
|
+
put_updated_resource(edit_url, resource)
|
7
|
+
end
|
8
|
+
# return count
|
9
|
+
collection.size
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "dm-keeper-adapter"
|
2
|
+
require "nokogiri"
|
3
|
+
|
4
|
+
module Keeper
|
5
|
+
class ContainerEntry
|
6
|
+
attr_reader :name, :description, :size, :postmaster
|
7
|
+
#
|
8
|
+
# parse
|
9
|
+
# <tr class="title">
|
10
|
+
# <td colspan="2"><b>product</b></td></tr>
|
11
|
+
# <tr><td><b>Description:</b></td><td>Container for storing product definitions from NPP.</td></tr>
|
12
|
+
# <tr><td><b>Documents:</b></td><td>#325</td></tr>
|
13
|
+
# <tr><td><b>Update notification class: </b></td><td>null</td></tr>
|
14
|
+
# <tr><td><b>Read-only: </b></td><td>false</td></tr>
|
15
|
+
# <tr><td><b>Notification debug: </b></td><td>false</td></tr>
|
16
|
+
# <tr><td><b>Notification postmaster: </b></td><td>tschmidt@suse.de</td></tr>
|
17
|
+
#
|
18
|
+
def initialize node
|
19
|
+
# $stderr.puts "ContainerEntry(#{node})"
|
20
|
+
@name = node.xpath("./td/b").text
|
21
|
+
while node = node.next do # next <tr>
|
22
|
+
break if node['class']
|
23
|
+
key = value = nil
|
24
|
+
node.children.each do |child| # iterate over <td>
|
25
|
+
if child.child.name == "b" # <td><b>
|
26
|
+
key = child.child.text
|
27
|
+
else
|
28
|
+
value = child.text
|
29
|
+
end
|
30
|
+
if key && value
|
31
|
+
case key
|
32
|
+
when "Description:": @description = value
|
33
|
+
when "Documents:": @size = value.to_i
|
34
|
+
when "Notification postmaster: ": @postmaster = value
|
35
|
+
end
|
36
|
+
key = value = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
def to_s
|
42
|
+
"#{@name}: #{@description}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
class Container
|
46
|
+
def initialize adapter
|
47
|
+
@adapter = adapter
|
48
|
+
@entries = []
|
49
|
+
node = adapter.get "/info"
|
50
|
+
node.xpath("//tr[@class='title']").each do |node|
|
51
|
+
@entries << ContainerEntry.new(node)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def each
|
55
|
+
@entries.each { |e| yield e }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Feature for datamapper
|
3
|
+
#
|
4
|
+
require 'rubygems'
|
5
|
+
require 'dm-core'
|
6
|
+
|
7
|
+
class Feature
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
def self.xpathmap
|
11
|
+
{ :id => "@k:id", :milestone => "productcontext/milestone/name", :actors => "actor",
|
12
|
+
:requester => "actor[role='requester']/person/email",
|
13
|
+
:productmgr => "actor[role='productmanager']/person/email",
|
14
|
+
:projectmgr => "actor[role='projectmanager']/person/email",
|
15
|
+
:engmgr => "actor[role='teamleader']/person/email",
|
16
|
+
:developer => "actor[role='developer']/person/email"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
def self.xmlnamespaces
|
20
|
+
{ "k" => "http://inttools.suse.de/sxkeeper/schema/keeper" }
|
21
|
+
end
|
22
|
+
|
23
|
+
property :id, Integer, :key => true
|
24
|
+
property :title, String
|
25
|
+
property :requester, String
|
26
|
+
property :productmgr, String
|
27
|
+
property :projectmgr, String
|
28
|
+
property :engmgr, String
|
29
|
+
property :developer, String
|
30
|
+
property :milestone, String
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
class Product
|
4
|
+
include DataMapper::Resource
|
5
|
+
property :id, Integer, :key => true
|
6
|
+
has 1, :productline
|
7
|
+
property :fatename, String
|
8
|
+
property :shortname, String
|
9
|
+
property :longname, String
|
10
|
+
property :bugzillaname, String
|
11
|
+
has n, :milestone
|
12
|
+
has n, :component
|
13
|
+
property :releasedate, Date
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
class Relation
|
4
|
+
include DataMapper::Resource
|
5
|
+
def self.xpathmap
|
6
|
+
{ :target => "@target", :sort_position => "@sortPosition" }
|
7
|
+
end
|
8
|
+
property :target, Integer, :key => true
|
9
|
+
property :sort_position, Integer
|
10
|
+
has 1, :parent, self
|
11
|
+
belongs_to :relation, :required => false
|
12
|
+
belongs_to :relationtree, :required => false
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'dm-types'
|
4
|
+
|
5
|
+
require 'keeper/relation'
|
6
|
+
|
7
|
+
class Relationtree
|
8
|
+
include DataMapper::Resource
|
9
|
+
def self.xpathmap
|
10
|
+
{ :id => "@k:id" }
|
11
|
+
end
|
12
|
+
def self.xmlnamespaces
|
13
|
+
{ "k" => "http://inttools.suse.de/sxkeeper/schema/keeper" }
|
14
|
+
end
|
15
|
+
property :id, Integer, :key => true
|
16
|
+
property :title, String
|
17
|
+
property :description, String
|
18
|
+
# has n, :relations
|
19
|
+
property :relations, Csv
|
20
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Keeper
|
2
|
+
class Containers
|
3
|
+
|
4
|
+
def initialize(name, options)
|
5
|
+
super
|
6
|
+
require 'net/https'
|
7
|
+
require 'uri'
|
8
|
+
@uri = URI.parse(options[:url])
|
9
|
+
@username = options[:username]
|
10
|
+
@password = options[:password]
|
11
|
+
@connection = Net::HTTP.new( @uri.host, @uri.port )
|
12
|
+
# from http://www.rubyinside.com/nethttp-cheat-sheet-2940.html
|
13
|
+
@connection.use_ssl = true if @uri.scheme == "https"
|
14
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def categories
|
19
|
+
@categories ||= values_of "list_categories", "/value"
|
20
|
+
end
|
21
|
+
def partners
|
22
|
+
@partners ||= values_of "list_partner", "/value"
|
23
|
+
end
|
24
|
+
def roles
|
25
|
+
@roles ||= values_of "list_roles"
|
26
|
+
end
|
27
|
+
def viewparts
|
28
|
+
@viewparts ||= values_of("list_view_parts")
|
29
|
+
end
|
30
|
+
def xmlfields
|
31
|
+
@xmlfields ||= values_of("list_xmlfields")
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(path)
|
35
|
+
# $stderr.puts "Get #{@uri.path} + #{path}"
|
36
|
+
request = Net::HTTP::Get.new @uri.path + path
|
37
|
+
request.basic_auth @username, @password
|
38
|
+
response = @connection.request request
|
39
|
+
raise ArgumentError unless response
|
40
|
+
raise( RuntimeError, "Server returned #{response.code}" ) unless response.code.to_i == 200
|
41
|
+
# $stderr.puts "#{response.inspect}"
|
42
|
+
# response.each { |k,v| $stderr.puts "#{k}: #{v}" }
|
43
|
+
# $stderr.puts "Encoding >#{response['content-encoding']}<"
|
44
|
+
# check
|
45
|
+
# content-type: text/xml;charset=UTF-8
|
46
|
+
# content-encoding: gzip
|
47
|
+
raise TypeError unless response['content-type'] =~ /xml/
|
48
|
+
body = response.body
|
49
|
+
if response['content-encoding'] == "gzip"
|
50
|
+
require 'zlib'
|
51
|
+
require 'stringio'
|
52
|
+
# http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-string-in-ruby
|
53
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
54
|
+
end
|
55
|
+
Nokogiri::XML body
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def values_of id, detail = nil
|
60
|
+
# STDERR.puts "----------------------\nextract_values #{id}\n#-------------------"
|
61
|
+
|
62
|
+
if detail
|
63
|
+
result = []
|
64
|
+
else
|
65
|
+
result = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
valuelist.xpath("//valuelist[@id='#{id}']/item#{detail}").each do |node|
|
69
|
+
if detail
|
70
|
+
# node = <value>foo</value>
|
71
|
+
result << node.text
|
72
|
+
else
|
73
|
+
# node = <item><value>...</value><property name='...'>...</property>
|
74
|
+
# with multiple properties
|
75
|
+
props = {}
|
76
|
+
# puts "Node #{node}"
|
77
|
+
# puts "Value #{node.xpath('./value').text}"
|
78
|
+
node.xpath("./property").each do |prop|
|
79
|
+
# puts "prop #{prop}"
|
80
|
+
# puts "name #{prop['name']}"
|
81
|
+
# puts "content #{prop.content}"
|
82
|
+
props[prop['name']] = prop.text
|
83
|
+
end
|
84
|
+
# puts "Props #{props.inspect}"
|
85
|
+
result[node.xpath("./value").text] = props
|
86
|
+
end
|
87
|
+
end
|
88
|
+
raise RuntimeError if result.empty?
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
def valuelist
|
93
|
+
@valuelist ||= get "/valuelist"
|
94
|
+
raise RuntimeError if @valuelist.nil?
|
95
|
+
@valuelist
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/property/xml.rb
ADDED
data/script/README.rdoc
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
= Sample scripts
|
2
|
+
|
3
|
+
keeper.novell.com also provides structure information about the
|
4
|
+
entities stored in its database.
|
5
|
+
|
6
|
+
== generate_class.rb
|
7
|
+
generate_class.rb is a sample Ruby script showing how to use this
|
8
|
+
information to generate Ruby classes for use with DataMapper.
|
9
|
+
|
10
|
+
== show-containers.rb
|
11
|
+
show-containers.rb displays information about the containers (aka
|
12
|
+
database tables) provides by keeper.novell.com
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
#
|
3
|
+
# Generate/update class definition for 'Feature'
|
4
|
+
|
5
|
+
require 'lib/dm-keeper-adapter'
|
6
|
+
|
7
|
+
adapter = DataMapper.setup(:default, :adapter => 'keeper',
|
8
|
+
:url => 'https://keeper.novell.com/sxkeeper')
|
9
|
+
|
10
|
+
adapter.update_featureclass "feature.rb"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#
|
2
|
+
# generate or update the class definition for 'Feature'
|
3
|
+
#
|
4
|
+
def update_featureclass path
|
5
|
+
partsrevision = 0
|
6
|
+
xmlfieldsrevision = 0
|
7
|
+
if File.exists? path
|
8
|
+
require path
|
9
|
+
partsrevision = Feature.partsrevision
|
10
|
+
xmlfieldsrevision = Feature.xmlfieldsrevision
|
11
|
+
end
|
12
|
+
current_partsrevision = revision_of('list_view_parts').to_i
|
13
|
+
current_xmlfieldsrevision = revision_of('list_xmlfields').to_i
|
14
|
+
if partsrevision < current_partsrevision || xmlfieldsrevision < current_xmlfieldsrevision
|
15
|
+
# class needs regeneration
|
16
|
+
File.open path, "w+" do |f|
|
17
|
+
f.puts("# File generated by dm-keeper-adapter on #{Time.new.asctime}")
|
18
|
+
f.puts("require 'rubygems'")
|
19
|
+
f.puts("require 'dm-core'")
|
20
|
+
f.puts("class Feature")
|
21
|
+
f.puts(" def Feature.partsrevision")
|
22
|
+
f.puts(" #{current_partsrevision}")
|
23
|
+
f.puts(" end")
|
24
|
+
f.puts(" def Feature.xmlfieldsrevision")
|
25
|
+
f.puts(" #{current_xmlfieldsrevision}")
|
26
|
+
f.puts(" end")
|
27
|
+
f.puts(" include DataMapper::Resource\n")
|
28
|
+
f.puts(" property :id, Integer, :key => true")
|
29
|
+
properties = []
|
30
|
+
viewparts.each_key do |k|
|
31
|
+
n = k.tr(" ","_").downcase
|
32
|
+
properties << n
|
33
|
+
f.puts(" property :#{n}, String")
|
34
|
+
end
|
35
|
+
xmlfields.each_key do |k|
|
36
|
+
n = k.tr(" ","_").downcase
|
37
|
+
next if n == '.'
|
38
|
+
next if properties.include? n
|
39
|
+
f.puts(" property :#{n}, String")
|
40
|
+
end
|
41
|
+
f.puts("end")
|
42
|
+
end
|
43
|
+
else
|
44
|
+
puts "#{path} is up to date"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def revision_of id
|
49
|
+
node = valuelist.xpath("//valuelist[@id='#{id}']").first
|
50
|
+
node.attribute_with_ns('revision', 'http://inttools.suse.de/sxkeeper/schema/keeper').value
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#
|
2
|
+
# show-containers.rb
|
3
|
+
#
|
4
|
+
# Demo script to extract available 'containers' (aka database tables)
|
5
|
+
# from FATE (keeper.suse.de) using DataMapper
|
6
|
+
#
|
7
|
+
|
8
|
+
$: << File.join(File.dirname(__FILE__), "..", "dm-keeper-adapter", "lib")
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'dm-core'
|
12
|
+
require 'keeper/container'
|
13
|
+
|
14
|
+
DataMapper::Logger.new($stdout, :debug)
|
15
|
+
keeper = DataMapper.setup(:default, :adapter => 'keeper',
|
16
|
+
:url => 'https://keeper.novell.com/sxkeeper')
|
17
|
+
|
18
|
+
c = Keeper::Container.new keeper
|
19
|
+
c.each do |e|
|
20
|
+
puts e
|
21
|
+
end
|
22
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'helper')
|
2
|
+
|
3
|
+
class Get_bug_test < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_get_bug
|
6
|
+
DataMapper::Logger.new(STDOUT, :debug)
|
7
|
+
keeper = DataMapper.setup(:default,
|
8
|
+
:adapter => 'keeper',
|
9
|
+
:url => 'https://keeper.novell.com/sxkeeper')
|
10
|
+
|
11
|
+
require 'keeper/feature'
|
12
|
+
DataMapper.finalize
|
13
|
+
|
14
|
+
feature = Feature.get(312814)
|
15
|
+
assert feature
|
16
|
+
puts "Feature #{feature.inspect}"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'helper')
|
2
|
+
|
3
|
+
class Get_relationtree_test < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_get_relationtree
|
6
|
+
DataMapper::Logger.new(STDOUT, :debug)
|
7
|
+
keeper = DataMapper.setup(:default,
|
8
|
+
:adapter => 'keeper',
|
9
|
+
:url => 'https://keeper.novell.com/sxkeeper')
|
10
|
+
|
11
|
+
require 'keeper/relationtree'
|
12
|
+
DataMapper.finalize
|
13
|
+
|
14
|
+
tree = Relationtree.get(1137)
|
15
|
+
assert tree
|
16
|
+
puts "Relationtree #{tree.inspect}"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-keeper-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- "Klaus K\xC3\xA4mpf"
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-27 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: inifile
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 13
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 4
|
33
|
+
- 1
|
34
|
+
version: 0.4.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: nokogiri
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 7
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 4
|
49
|
+
version: "1.4"
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
description: Use it in Ruby applications to access FATE
|
53
|
+
email:
|
54
|
+
- kkaempf@suse.de
|
55
|
+
executables: []
|
56
|
+
|
57
|
+
extensions: []
|
58
|
+
|
59
|
+
extra_rdoc_files: []
|
60
|
+
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- README.rdoc
|
64
|
+
- Rakefile
|
65
|
+
- dm-keeper-adapter.gemspec
|
66
|
+
- lib/dm-keeper-adapter.rb
|
67
|
+
- lib/dm-keeper-adapter/create.rb
|
68
|
+
- lib/dm-keeper-adapter/delete.rb
|
69
|
+
- lib/dm-keeper-adapter/misc.rb
|
70
|
+
- lib/dm-keeper-adapter/read.rb
|
71
|
+
- lib/dm-keeper-adapter/update.rb
|
72
|
+
- lib/dm-keeper-adapter/version.rb
|
73
|
+
- lib/keeper/container.rb
|
74
|
+
- lib/keeper/feature.rb
|
75
|
+
- lib/keeper/product.rb
|
76
|
+
- lib/keeper/productline.rb
|
77
|
+
- lib/keeper/relation.rb
|
78
|
+
- lib/keeper/relationtree.rb
|
79
|
+
- lib/keeper/valuelist.rb
|
80
|
+
- lib/property/xml.rb
|
81
|
+
- script/README.rdoc
|
82
|
+
- script/generate_class.rb
|
83
|
+
- script/genfeatureclass.rb
|
84
|
+
- script/show-containers.rb
|
85
|
+
- test/helper.rb
|
86
|
+
- test/test_get_feature.rb
|
87
|
+
- test/test_get_relationtree.rb
|
88
|
+
has_rdoc: true
|
89
|
+
homepage: ""
|
90
|
+
licenses: []
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
hash: 3
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
hash: 3
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
requirements: []
|
116
|
+
|
117
|
+
rubyforge_project: dm-keeper-adapter
|
118
|
+
rubygems_version: 1.5.2
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: A datamapper adapter for FATE (aka keeper.novell.com)
|
122
|
+
test_files:
|
123
|
+
- test/helper.rb
|
124
|
+
- test/test_get_feature.rb
|
125
|
+
- test/test_get_relationtree.rb
|